From: Remi Gacogne Date: Wed, 24 Jul 2019 17:04:09 +0000 (+0200) Subject: dnsdist: Add OCSP stapling (from files) for DoT and DoH X-Git-Tag: dnsdist-1.4.0-rc1~4^2~2 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=be3183eda2338f33fec7006cbeaf909d8068791f;p=pdns dnsdist: Add OCSP stapling (from files) for DoT and DoH The OCSP responses are loaded from files on startup, and reloaded when the reloadAllCertificates() command is issued. --- diff --git a/pdns/dnsdist-lua.cc b/pdns/dnsdist-lua.cc index 6a0e5211d..4195d428b 100644 --- a/pdns/dnsdist-lua.cc +++ b/pdns/dnsdist-lua.cc @@ -87,7 +87,7 @@ void resetLuaSideEffect() g_noLuaSideEffect = boost::logic::indeterminate; } -typedef std::unordered_map >, std::map > > localbind_t; +typedef std::unordered_map >, std::map, std::vector > > > localbind_t; static void parseLocalBindVars(boost::optional vars, bool& reusePort, int& tcpFastOpenQueueSize, std::string& interface, std::set& cpus) { @@ -1732,6 +1732,12 @@ void setupLuaConfig(bool client) frontend->d_customResponseHeaders.push_back(headerResponse); } } + if (vars->count("ocspResponses")) { + auto files = boost::get>>((*vars)["ocspResponses"]); + for (const auto& file : files) { + frontend->d_ocspFiles.push_back(file.second); + } + } } g_dohlocals.push_back(frontend); auto cs = std::unique_ptr(new ClientState(frontend->d_local, true, reusePort, tcpFastOpenQueueSize, interface, cpus)); @@ -1888,6 +1894,13 @@ void setupLuaConfig(bool client) } frontend->d_maxStoredSessions = value; } + + if (vars->count("ocspResponses")) { + auto files = boost::get>>((*vars)["ocspResponses"]); + for (const auto& file : files) { + frontend->d_ocspFiles.push_back(file.second); + } + } } try { diff --git a/pdns/dnsdistdist/docs/reference/config.rst b/pdns/dnsdistdist/docs/reference/config.rst index 87564cc19..d2e56765f 100644 --- a/pdns/dnsdistdist/docs/reference/config.rst +++ b/pdns/dnsdistdist/docs/reference/config.rst @@ -127,6 +127,7 @@ Listen Sockets * ``ciphersTLS13``: str - The TLS ciphers to use for TLS 1.3, in OpenSSL format. * ``serverTokens``: str - The content of the Server: HTTP header returned by dnsdist. The default is "h2o/dnsdist". * ``customResponseHeaders={}``: table - Set custom HTTP header(s) returned by dnsdist. + * ``ocspResponses``: list - List of files containing OCSP responses, in the same order than the certificates and keys, that will be used to provide OCSP stapling responses. .. function:: addTLSLocal(address, certFile(s), keyFile(s) [, options]) @@ -137,7 +138,7 @@ Listen Sockets .. versionchanged:: 1.3.3 ``numberOfStoredSessions`` option added. .. versionchanged:: 1.4.0 - ``ciphersTLS13`` option added. + ``ciphersTLS13`` and ``ocspResponses`` options added. Listen on the specified address and TCP port for incoming DNS over TLS connections, presenting the specified X.509 certificate. @@ -161,6 +162,7 @@ Listen Sockets * ``ticketsKeysRotationDelay``: int - Set the delay before the TLS tickets key is rotated, in seconds. Default is 43200 (12h). * ``sessionTickets``: bool - Whether session resumption via session tickets is enabled. Default is true, meaning tickets are enabled. * ``numberOfStoredSessions``: int - The maximum number of sessions kept in memory at the same time. At this time this is only supported by the OpenSSL provider, as stored sessions are not supported with the GnuTLS one. Default is 20480. Setting this value to 0 disables stored session entirely. + * ``ocspResponses``: list - List of files containing OCSP responses, in the same order than the certificates and keys, that will be used to provide OCSP stapling responses. .. function:: setLocal(address[, options]) diff --git a/pdns/dnsdistdist/doh.cc b/pdns/dnsdistdist/doh.cc index f932c49f3..bb4b6da94 100644 --- a/pdns/dnsdistdist/doh.cc +++ b/pdns/dnsdistdist/doh.cc @@ -82,6 +82,8 @@ public: } } + std::map d_ocspResponses; + private: h2o_accept_ctx_t d_h2o_accept_ctx; std::atomic d_refcnt{1}; @@ -728,7 +730,16 @@ static int create_listener(const ComboAddress& addr, std::shared_ptr getTLSContext(const std::vector>& pairs, const std::string& ciphers, const std::string& ciphers13) +static int ocsp_stapling_callback(SSL* ssl, void* arg) +{ + if (ssl == nullptr || arg == nullptr) { + return SSL_TLSEXT_ERR_NOACK; + } + const auto ocspMap = reinterpret_cast*>(arg); + return libssl_ocsp_stapling_callback(ssl, *ocspMap); +} + +static std::unique_ptr getTLSContext(const std::vector>& pairs, const std::string& ciphers, const std::string& ciphers13, const std::vector& ocspFiles, std::map& ocspResponses) { auto ctx = std::unique_ptr(SSL_CTX_new(SSLv23_server_method()), SSL_CTX_free); @@ -746,6 +757,7 @@ static std::unique_ptr getTLSContext(const std::vect SSL_CTX_set_ecdh_auto(ctx.get(), 1); #endif + std::vector keyTypes; /* load certificate and private key */ for (const auto& pair : pairs) { if (SSL_CTX_use_certificate_chain_file(ctx.get(), pair.first.c_str()) != 1) { @@ -756,6 +768,24 @@ static std::unique_ptr getTLSContext(const std::vect ERR_print_errors_fp(stderr); throw std::runtime_error("Failed to setup SSL/TLS for DoH listener, an error occurred while trying to load the DOH server private key file: " + pair.second); } + if (SSL_CTX_check_private_key(ctx.get()) != 1) { + ERR_print_errors_fp(stderr); + throw std::runtime_error("Failed to setup SSL/TLS for DoH listener, the key from '" + pair.second + "' does not match the certificate from '" + pair.first + "'"); + } + /* store the type of the new key, we might need it later to select the right OCSP stapling response */ + keyTypes.push_back(libssl_get_last_key_type(ctx)); + } + + if (!ocspFiles.empty()) { + try { + ocspResponses = libssl_load_ocsp_responses(ocspFiles, keyTypes); + + SSL_CTX_set_tlsext_status_cb(ctx.get(), &ocsp_stapling_callback); + SSL_CTX_set_tlsext_status_arg(ctx.get(), &ocspResponses); + } + catch(const std::exception& e) { + throw std::runtime_error("Unable to load OCSP responses for the SSL/TLS DoH listener: " + std::string(e.what())); + } } if (SSL_CTX_set_cipher_list(ctx.get(), ciphers.empty() == false ? ciphers.c_str() : DOH_DEFAULT_CIPHERS) != 1) { @@ -781,7 +811,9 @@ static void setupAcceptContext(DOHAcceptContext& ctx, DOHServerConfig& dsc, bool if (setupTLS) { auto tlsCtx = getTLSContext(dsc.df->d_certKeyPairs, dsc.df->d_ciphers, - dsc.df->d_ciphers13); + dsc.df->d_ciphers13, + dsc.df->d_ocspFiles, + ctx.d_ocspResponses); nativeCtx->ssl_ctx = tlsCtx.release(); } @@ -806,7 +838,9 @@ void DOHFrontend::setup() auto tlsCtx = getTLSContext(d_certKeyPairs, d_ciphers, - d_ciphers13); + d_ciphers13, + d_ocspFiles, + d_dsc->accept_ctx->d_ocspResponses); auto accept_ctx = d_dsc->accept_ctx->get(); accept_ctx->ssl_ctx = tlsCtx.release(); diff --git a/pdns/dnsdistdist/libssl.cc b/pdns/dnsdistdist/libssl.cc index 684a276c3..ce7e2660b 100644 --- a/pdns/dnsdistdist/libssl.cc +++ b/pdns/dnsdistdist/libssl.cc @@ -5,9 +5,13 @@ #ifdef HAVE_LIBSSL #include +#include +#include #include + #include #include +#include #include #include @@ -86,4 +90,131 @@ void unregisterOpenSSLUser() #endif } +int libssl_ocsp_stapling_callback(SSL* ssl, const std::map& ocspMap) +{ + auto pkey = SSL_get_privatekey(ssl); + if (pkey == nullptr) { + return SSL_TLSEXT_ERR_NOACK; + } + + /* look for an OCSP response for the corresponding private key type (RSA, ECDSA..) */ + const auto& data = ocspMap.find(EVP_PKEY_base_id(pkey)); + if (data == ocspMap.end()) { + return SSL_TLSEXT_ERR_NOACK; + } + + /* we need to allocate a copy because OpenSSL will free the pointer passed to SSL_set_tlsext_status_ocsp_resp() */ + void* copy = OPENSSL_malloc(data->second.size()); + if (copy == nullptr) { + return SSL_TLSEXT_ERR_NOACK; + } + + memcpy(copy, data->second.data(), data->second.size()); + SSL_set_tlsext_status_ocsp_resp(ssl, copy, data->second.size()); + return SSL_TLSEXT_ERR_OK; +} + +static bool libssl_validate_ocsp_response(const std::string& response) +{ + auto responsePtr = reinterpret_cast(response.data()); + std::unique_ptr resp(d2i_OCSP_RESPONSE(nullptr, &responsePtr, response.size()), OCSP_RESPONSE_free); + if (resp == nullptr) { + throw std::runtime_error("Unable to parse OCSP response"); + } + + int status = OCSP_response_status(resp.get()); + if (status != OCSP_RESPONSE_STATUS_SUCCESSFUL) { + throw std::runtime_error("OCSP response status is not successful: " + std::to_string(status)); + } + + std::unique_ptr basic(OCSP_response_get1_basic(resp.get()), OCSP_BASICRESP_free); + if (basic == nullptr) { + throw std::runtime_error("Error getting a basic OCSP response"); + } + + if (OCSP_resp_count(basic.get()) != 1) { + throw std::runtime_error("More than one single response in an OCSP basic response"); + } + + auto singleResponse = OCSP_resp_get0(basic.get(), 0); + if (singleResponse == nullptr) { + throw std::runtime_error("Error getting a single response from the basic OCSP response"); + } + + int reason; + ASN1_GENERALIZEDTIME* revTime = nullptr; + ASN1_GENERALIZEDTIME* thisUpdate = nullptr; + ASN1_GENERALIZEDTIME* nextUpdate = nullptr; + + auto singleResponseStatus = OCSP_single_get0_status(singleResponse, &reason, &revTime, &thisUpdate, &nextUpdate); + if (singleResponseStatus != V_OCSP_CERTSTATUS_GOOD) { + throw std::runtime_error("Invalid status for OCSP single response (" + std::to_string(singleResponseStatus) + ")"); + } + if (thisUpdate == nullptr || nextUpdate == nullptr) { + throw std::runtime_error("Error getting validity of OCSP single response"); + } + + auto validityResult = OCSP_check_validity(thisUpdate, nextUpdate, /* 5 minutes of leeway */ 5 * 60, -1); + if (validityResult == 0) { + throw std::runtime_error("OCSP single response is not yet, or no longer, valid"); + } + + return true; +} + +std::map libssl_load_ocsp_responses(const std::vector& ocspFiles, std::vector keyTypes) +{ + std::map ocspResponses; + + if (ocspFiles.size() > keyTypes.size()) { + throw std::runtime_error("More OCSP files than certificates and keys loaded!"); + } + + size_t count = 0; + for (const auto& filename : ocspFiles) { + std::ifstream file(filename, std::ios::binary); + std::string content; + while(file) { + char buffer[4096]; + file.read(buffer, sizeof(buffer)); + if (file.bad()) { + file.close(); + throw std::runtime_error("Unable to load OCSP response from '" + filename + "'"); + } + content.append(buffer, file.gcount()); + } + file.close(); + + try { + libssl_validate_ocsp_response(content); + ocspResponses.insert({keyTypes.at(count), std::move(content)}); + } + catch (const std::exception& e) { + throw std::runtime_error("Error checking the validity of OCSP response from '" + filename + "': " + e.what()); + } + ++count; + } + + return ocspResponses; +} + +int libssl_get_last_key_type(std::unique_ptr& ctx) +{ +#if (OPENSSL_VERSION_NUMBER >= 0x10002000L && !defined LIBRESSL_VERSION_NUMBER) + auto pkey = SSL_CTX_get0_privatekey(ctx.get()); +#else + auto temp = std::unique_ptr(SSL_new(ctx.get()), SSL_free); + if (!temp) { + return -1; + } + auto pkey = SSL_get_privatekey(temp.get()); +#endif + + if (!pkey) { + return -1; + } + + return EVP_PKEY_base_id(pkey); +} + #endif /* HAVE_LIBSSL */ diff --git a/pdns/dnsdistdist/libssl.hh b/pdns/dnsdistdist/libssl.hh index a735815d5..0853400a6 100644 --- a/pdns/dnsdistdist/libssl.hh +++ b/pdns/dnsdistdist/libssl.hh @@ -1,4 +1,21 @@ #pragma once +#include +#include +#include +#include + +#include "config.h" + +#ifdef HAVE_LIBSSL +#include + void registerOpenSSLUser(); void unregisterOpenSSLUser(); + +int libssl_ocsp_stapling_callback(SSL* ssl, const std::map& ocspMap); + +std::map libssl_load_ocsp_responses(const std::vector& ocspFiles, std::vector keyTypes); +int libssl_get_last_key_type(std::unique_ptr& ctx); + +#endif /* HAVE_LIBSSL */ diff --git a/pdns/dnsdistdist/tcpiohandler.cc b/pdns/dnsdistdist/tcpiohandler.cc index 5e506e24d..f14c6e8dd 100644 --- a/pdns/dnsdistdist/tcpiohandler.cc +++ b/pdns/dnsdistdist/tcpiohandler.cc @@ -422,6 +422,7 @@ public: SSL_CTX_sess_set_cache_size(d_tlsCtx.get(), fe.d_maxStoredSessions); } + std::vector keyTypes; for (const auto& pair : fe.d_certKeyPairs) { if (SSL_CTX_use_certificate_chain_file(d_tlsCtx.get(), pair.first.c_str()) != 1) { ERR_print_errors_fp(stderr); @@ -431,6 +432,25 @@ public: ERR_print_errors_fp(stderr); throw std::runtime_error("Error loading key from " + pair.second + " for the TLS context on " + fe.d_addr.toStringWithPort()); } + if (SSL_CTX_check_private_key(d_tlsCtx.get()) != 1) { + ERR_print_errors_fp(stderr); + throw std::runtime_error("Key from '" + pair.second + "' does not match the certificate from '" + pair.first + "' for the TLS context on " + fe.d_addr.toStringWithPort()); + } + + /* store the type of the new key, we might need it later to select the right OCSP stapling response */ + keyTypes.push_back(libssl_get_last_key_type(d_tlsCtx)); + } + + if (!fe.d_ocspFiles.empty()) { + try { + d_ocspResponses = libssl_load_ocsp_responses(fe.d_ocspFiles, keyTypes); + } + catch(const std::exception& e) { + throw std::runtime_error("Error loading responses for the TLS context on " + fe.d_addr.toStringWithPort() + ": " + e.what()); + } + + SSL_CTX_set_tlsext_status_cb(d_tlsCtx.get(), &OpenSSLTLSIOCtx::ocspStaplingCb); + SSL_CTX_set_tlsext_status_arg(d_tlsCtx.get(), &d_ocspResponses); } if (!fe.d_ciphers.empty()) { @@ -512,6 +532,15 @@ public: return 1; } + static int ocspStaplingCb(SSL* ssl, void* arg) + { + if (ssl == nullptr || arg == nullptr) { + return SSL_TLSEXT_ERR_NOACK; + } + const auto ocspMap = reinterpret_cast*>(arg); + return libssl_ocsp_stapling_callback(ssl, *ocspMap); + } + std::unique_ptr getConnection(int socket, unsigned int timeout, time_t now) override { handleTicketsKeyRotation(now); @@ -562,6 +591,7 @@ public: private: OpenSSLTLSTicketKeysRing d_ticketKeys; + std::map d_ocspResponses; std::unique_ptr d_tlsCtx; static std::atomic s_users; }; @@ -918,6 +948,15 @@ public: } } + size_t count = 0; + for (const auto& file : fe.d_ocspFiles) { + rc = gnutls_certificate_set_ocsp_status_request_file(d_creds.get(), file.c_str(), count); + if (rc != GNUTLS_E_SUCCESS) { + throw std::runtime_error("Error loading OCSP response from file '" + file + "' for certificate ('" + fe.d_certKeyPairs.at(count).first + "') and key ('" + fe.d_certKeyPairs.at(count).second + "') for TLS context on " + fe.d_addr.toStringWithPort() + ": " + gnutls_strerror(rc)); + } + ++count; + } + #if GNUTLS_VERSION_NUMBER >= 0x030600 rc = gnutls_certificate_set_known_dh_params(d_creds.get(), GNUTLS_SEC_PARAM_HIGH); if (rc != GNUTLS_E_SUCCESS) { diff --git a/pdns/doh.hh b/pdns/doh.hh index b7b9bfe89..3ff13ed6b 100644 --- a/pdns/doh.hh +++ b/pdns/doh.hh @@ -7,6 +7,7 @@ struct DOHFrontend { std::shared_ptr d_dsc{nullptr}; std::vector> d_certKeyPairs; + std::vector d_ocspFiles; std::string d_ciphers; std::string d_ciphers13; std::string d_serverTokens{"h2o/dnsdist"}; diff --git a/pdns/tcpiohandler.hh b/pdns/tcpiohandler.hh index 3e7f52b08..1718c664c 100644 --- a/pdns/tcpiohandler.hh +++ b/pdns/tcpiohandler.hh @@ -139,6 +139,7 @@ public: } std::vector> d_certKeyPairs; + std::vector d_ocspFiles; ComboAddress d_addr; std::string d_ciphers; std::string d_ciphers13;