]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
Merge pull request #8127 from Habbie/auth-4.2.0-rc3-docs
authoraerique <erik.winkels@powerdns.com>
Fri, 9 Aug 2019 10:50:48 +0000 (12:50 +0200)
committerGitHub <noreply@github.com>
Fri, 9 Aug 2019 10:50:48 +0000 (12:50 +0200)
changelog and secpoll for auth 4.2.0-rc3

16 files changed:
build-scripts/travis.sh
pdns/dnsdist-lua.cc
pdns/dnsdist.cc
pdns/dnsdistdist/docs/advanced/tuning.rst
pdns/dnsdistdist/docs/reference/config.rst
pdns/dnsdistdist/docs/reference/tuning.rst
pdns/dnsdistdist/doh.cc
pdns/dnsdistdist/libssl.cc
pdns/dnsdistdist/libssl.hh
pdns/dnsdistdist/m4/dnsdist_with_libssl.m4
pdns/dnsdistdist/tcpiohandler.cc
pdns/doh.hh
pdns/tcpiohandler.hh
regression-tests.dnsdist/.gitignore
regression-tests.dnsdist/runtests
regression-tests.dnsdist/test_OCSP.py [new file with mode: 0644]

index d4275eb590cb689045bb65846bd9564b4f89856f..99ca9043edc5f473b43e1484b737adc00b40e15a 100755 (executable)
@@ -634,7 +634,7 @@ test_recursor() {
 
 test_dnsdist(){
   run "cd regression-tests.dnsdist"
-  run "DNSDISTBIN=$HOME/dnsdist/bin/dnsdist ./runtests -v --ignore-files='(?:^\.|^_,|^setup\.py$|^test_DOH\.py$)'"
+  run "DNSDISTBIN=$HOME/dnsdist/bin/dnsdist ./runtests -v --ignore-files='(?:^\.|^_,|^setup\.py$|^test_DOH\.py$|^test_OCSP\.py$)'"
   run "rm -f ./DNSCryptResolver.cert ./DNSCryptResolver.key"
   run "cd .."
 }
index 3e5cf38b7af805ecb282af9d2a5594d49098a28f..5ee4eedf05fdc6e717ed4b1b085c268972df7ac1 100644 (file)
 #include "protobuf.hh"
 #include "sodcrypto.hh"
 
+#ifdef HAVE_LIBSSL
+#include "libssl.hh"
+#endif
+
 #include <boost/logic/tribool.hpp>
 #include <boost/lexical_cast.hpp>
 
@@ -87,7 +91,7 @@ void resetLuaSideEffect()
   g_noLuaSideEffect = boost::logic::indeterminate;
 }
 
-typedef std::unordered_map<std::string, boost::variant<bool, int, std::string, std::vector<std::pair<int,int> >, std::map<std::string,std::string> > > localbind_t;
+typedef std::unordered_map<std::string, boost::variant<bool, int, std::string, std::vector<std::pair<int,int> >, std::vector<std::pair<int, std::string> >, std::map<std::string,std::string>  > > localbind_t;
 
 static void parseLocalBindVars(boost::optional<localbind_t> vars, bool& reusePort, int& tcpFastOpenQueueSize, std::string& interface, std::set<int>& cpus)
 {
@@ -1732,6 +1736,12 @@ void setupLuaConfig(bool client)
           frontend->d_customResponseHeaders.push_back(headerResponse);
         }
       }
+      if (vars->count("ocspResponses")) {
+        auto files = boost::get<std::vector<std::pair<int, std::string>>>((*vars)["ocspResponses"]);
+        for (const auto& file : files) {
+          frontend->d_ocspFiles.push_back(file.second);
+        }
+      }
     }
     g_dohlocals.push_back(frontend);
     auto cs = std::unique_ptr<ClientState>(new ClientState(frontend->d_local, true, reusePort, tcpFastOpenQueueSize, interface, cpus));
@@ -1888,6 +1898,13 @@ void setupLuaConfig(bool client)
             }
             frontend->d_maxStoredSessions = value;
           }
+
+          if (vars->count("ocspResponses")) {
+            auto files = boost::get<std::vector<std::pair<int, std::string>>>((*vars)["ocspResponses"]);
+            for (const auto& file : files) {
+              frontend->d_ocspFiles.push_back(file.second);
+            }
+          }
         }
 
         try {
@@ -2026,6 +2043,12 @@ void setupLuaConfig(bool client)
       });
 
     g_lua.writeFunction("setAllowEmptyResponse", [](bool allow) { g_allowEmptyResponse=allow; });
+
+#if defined(HAVE_LIBSSL) && defined(HAVE_OCSP_BASIC_SIGN)
+    g_lua.writeFunction("generateOCSPResponse", [](const std::string& certFile, const std::string& caCert, const std::string& caKey, const std::string& outFile, int ndays, int nmin) {
+      return libssl_generate_ocsp_response(certFile, caCert, caKey, outFile, ndays, nmin);
+    });
+#endif /* HAVE_LIBSSL && HAVE_OCSP_BASIC_SIGN*/
 }
 
 vector<std::function<void(void)>> setupLua(bool client, const std::string& config)
index 46ad4dfa39de1a1f83a820c74fd663096eef739a..e9ca2aff0d4d326fb034c8f04a947fc6283d7624 100644 (file)
@@ -83,7 +83,7 @@ bool g_verbose;
 struct DNSDistStats g_stats;
 MetricDefinitionStorage g_metricDefinitions;
 
-uint16_t g_maxOutstanding{10240};
+uint16_t g_maxOutstanding{std::numeric_limits<uint16_t>::max()};
 bool g_verboseHealthChecks{false};
 uint32_t g_staleCacheEntriesTTL{0};
 bool g_syslog{true};
index cf72ca7c51559bd2674a9e190075ba034d54d294..4fbbd171b703537924583e27d89ec3ad66e9e8d6 100644 (file)
@@ -25,7 +25,7 @@ This might cause issues if some connections are taking a very long time, since i
 The experimental :func:`setTCPUseSinglePipe` directive can be used so that all the incoming TCP connections are put into a single queue and handled by the first TCP worker available.
 
 When dispatching UDP queries to backend servers, dnsdist keeps track of at most **n** outstanding queries for each backend.
-This number **n** can be tuned by the :func:`setMaxUDPOutstanding` directive, defaulting to 10240, with a maximum value of 65535.
+This number **n** can be tuned by the :func:`setMaxUDPOutstanding` directive, defaulting to 10240 (65535 since 1.4.0), with a maximum value of 65535.
 Large installations are advised to increase the default value at the cost of a slightly increased memory usage.
 
 Most of the query processing is done in C++ for maximum performance, but some operations are executed in Lua for maximum flexibility:
index 87564cc19344367db8508298b1400c2677335765..d2e56765f8401bde8768a892a9e2a2100354c843 100644 (file)
@@ -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])
 
index 0f63a5145446e0000b1a4a8248a5c6bc87617566..b1dd58c5d6a43ef230cc23928efda1d666799b3d 100644 (file)
@@ -32,8 +32,10 @@ Tuning related functions
   :param int num:
 
 .. function:: setMaxUDPOutstanding(num)
+  .. versionchanged:: 1.4.0
+    Before 1.4.0 the default value was 10240
 
-  Set the maximum number of outstanding UDP queries to a given backend server. This can only be set at configuration time and defaults to 10240
+  Set the maximum number of outstanding UDP queries to a given backend server. This can only be set at configuration time and defaults to 65535 (10240 before 1.4.0)
 
   :param int num:
 
index 2453a2f3577d1169100f4d0973a606ad618c51e7..a5e187567764b2ebcdd8f0322ae9a4b251b0edd3 100644 (file)
@@ -82,6 +82,8 @@ public:
     }
   }
 
+  std::map<int, std::string> d_ocspResponses;
+
 private:
   h2o_accept_ctx_t d_h2o_accept_ctx;
   std::atomic<uint64_t> d_refcnt{1};
@@ -873,7 +875,16 @@ static int create_listener(const ComboAddress& addr, std::shared_ptr<DOHServerCo
   return 0;
 }
 
-static std::unique_ptr<SSL_CTX, void(*)(SSL_CTX*)> getTLSContext(const std::vector<std::pair<std::string, std::string>>& 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<std::map<int, std::string>*>(arg);
+  return libssl_ocsp_stapling_callback(ssl, *ocspMap);
+}
+
+static std::unique_ptr<SSL_CTX, void(*)(SSL_CTX*)> getTLSContext(const std::vector<std::pair<std::string, std::string>>& pairs, const std::string& ciphers, const std::string& ciphers13, const std::vector<std::string>& ocspFiles, std::map<int, std::string>& ocspResponses)
 {
   auto ctx = std::unique_ptr<SSL_CTX, void(*)(SSL_CTX*)>(SSL_CTX_new(SSLv23_server_method()), SSL_CTX_free);
 
@@ -891,6 +902,7 @@ static std::unique_ptr<SSL_CTX, void(*)(SSL_CTX*)> getTLSContext(const std::vect
   SSL_CTX_set_ecdh_auto(ctx.get(), 1);
 #endif
 
+  std::vector<int> 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) {
@@ -901,6 +913,28 @@ static std::unique_ptr<SSL_CTX, void(*)(SSL_CTX*)> 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 */
+    auto keyType = libssl_get_last_key_type(ctx);
+    if (keyType < 0) {
+      throw std::runtime_error("Failed to setup SSL/TLS for DoH listener, the key from '" + pair.second + "' has an unknown type");
+    }
+    keyTypes.push_back(keyType);
+  }
+
+  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) {
@@ -926,7 +960,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();
   }
@@ -951,7 +987,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();
index 684a276c314b05de11411c6dd78ddba9ae16f575..d0a2da24b07dbf8aaa6910d67c5c4548393570a6 100644 (file)
@@ -5,9 +5,13 @@
 #ifdef HAVE_LIBSSL
 
 #include <atomic>
+#include <fstream>
+#include <cstring>
 #include <pthread.h>
+
 #include <openssl/conf.h>
 #include <openssl/err.h>
+#include <openssl/ocsp.h>
 #include <openssl/rand.h>
 #include <openssl/ssl.h>
 
@@ -86,4 +90,178 @@ void unregisterOpenSSLUser()
 #endif
 }
 
+int libssl_ocsp_stapling_callback(SSL* ssl, const std::map<int, std::string>& 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<const unsigned char *>(response.data());
+  std::unique_ptr<OCSP_RESPONSE, void(*)(OCSP_RESPONSE*)> 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<OCSP_BASICRESP, void(*)(OCSP_BASICRESP*)> 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<int, std::string> libssl_load_ocsp_responses(const std::vector<std::string>& ocspFiles, std::vector<int> keyTypes)
+{
+  std::map<int, std::string> 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<SSL_CTX, void(*)(SSL_CTX*)>& 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, void(*)(SSL*)>(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);
+}
+
+#ifdef HAVE_OCSP_BASIC_SIGN
+bool libssl_generate_ocsp_response(const std::string& certFile, const std::string& caCert, const std::string& caKey, const std::string& outFile, int ndays, int nmin)
+{
+  const EVP_MD* rmd = EVP_sha256();
+
+  auto fp = std::unique_ptr<FILE, int(*)(FILE*)>(fopen(certFile.c_str(), "r"), fclose);
+  if (!fp) {
+    throw std::runtime_error("Unable to open '" + certFile + "' when loading the certificate to generate an OCSP response");
+  }
+  auto cert = std::unique_ptr<X509, void(*)(X509*)>(PEM_read_X509_AUX(fp.get(), nullptr, nullptr, nullptr), X509_free);
+
+  fp = std::unique_ptr<FILE, int(*)(FILE*)>(fopen(caCert.c_str(), "r"), fclose);
+  if (!fp) {
+    throw std::runtime_error("Unable to open '" + caCert + "' when loading the issuer certificate to generate an OCSP response");
+  }
+  auto issuer = std::unique_ptr<X509, void(*)(X509*)>(PEM_read_X509_AUX(fp.get(), nullptr, nullptr, nullptr), X509_free);
+  fp = std::unique_ptr<FILE, int(*)(FILE*)>(fopen(caKey.c_str(), "r"), fclose);
+  if (!fp) {
+    throw std::runtime_error("Unable to open '" + caKey + "' when loading the issuer key to generate an OCSP response");
+  }
+  auto issuerKey = std::unique_ptr<EVP_PKEY, void(*)(EVP_PKEY*)>(PEM_read_PrivateKey(fp.get(), nullptr, nullptr, nullptr), EVP_PKEY_free);
+  fp.reset();
+
+  auto bs = std::unique_ptr<OCSP_BASICRESP, void(*)(OCSP_BASICRESP*)>(OCSP_BASICRESP_new(), OCSP_BASICRESP_free);
+  auto thisupd = std::unique_ptr<ASN1_TIME, void(*)(ASN1_TIME*)>(X509_gmtime_adj(nullptr, 0), ASN1_TIME_free);
+  auto nextupd = std::unique_ptr<ASN1_TIME, void(*)(ASN1_TIME*)>(X509_time_adj_ex(nullptr, ndays, nmin * 60, nullptr), ASN1_TIME_free);
+
+  auto cid = std::unique_ptr<OCSP_CERTID, void(*)(OCSP_CERTID*)>(OCSP_cert_to_id(rmd, cert.get(), issuer.get()), OCSP_CERTID_free);
+  OCSP_basic_add1_status(bs.get(), cid.get(), V_OCSP_CERTSTATUS_GOOD, 0, nullptr, thisupd.get(), nextupd.get());
+
+  if (OCSP_basic_sign(bs.get(), issuer.get(), issuerKey.get(), rmd, nullptr, OCSP_NOCERTS) != 1) {
+    throw std::runtime_error("Error while signing the OCSP response");
+  }
+
+  auto resp = std::unique_ptr<OCSP_RESPONSE, void(*)(OCSP_RESPONSE*)>(OCSP_response_create(OCSP_RESPONSE_STATUS_SUCCESSFUL, bs.get()), OCSP_RESPONSE_free);
+  auto bio = std::unique_ptr<BIO, void(*)(BIO*)>(BIO_new_file(outFile.c_str(), "wb"), BIO_vfree);
+  if (!bio) {
+    throw std::runtime_error("Error opening file for writing the OCSP response");
+  }
+
+  // i2d_OCSP_RESPONSE_bio(bio.get(), resp.get()) is unusable from C++ because of an invalid cast
+  ASN1_i2d_bio((i2d_of_void*)i2d_OCSP_RESPONSE, bio.get(), (unsigned char*)resp.get());
+
+  return true;
+}
+#endif /* HAVE_OCSP_BASIC_SIGN */
+
 #endif /* HAVE_LIBSSL */
index a735815d542ad6313d9f6483b21897d6569f2ee5..f722e5cddea6bdec0f062bf6aa0c3bd1b22f8589 100644 (file)
@@ -1,4 +1,25 @@
 #pragma once
 
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "config.h"
+
+#ifdef HAVE_LIBSSL
+#include <openssl/ssl.h>
+
 void registerOpenSSLUser();
 void unregisterOpenSSLUser();
+
+int libssl_ocsp_stapling_callback(SSL* ssl, const std::map<int, std::string>& ocspMap);
+
+std::map<int, std::string> libssl_load_ocsp_responses(const std::vector<std::string>& ocspFiles, std::vector<int> keyTypes);
+int libssl_get_last_key_type(std::unique_ptr<SSL_CTX, void(*)(SSL_CTX*)>& ctx);
+
+#ifdef HAVE_OCSP_BASIC_SIGN
+bool libssl_generate_ocsp_response(const std::string& certFile, const std::string& caCert, const std::string& caKey, const std::string& outFile, int ndays, int nmin);
+#endif
+
+#endif /* HAVE_LIBSSL */
index 624a3d688f5ff20501355436c91937766340fab4..730b33c7fba366db994ec40c60a3a95883831b7c 100644 (file)
@@ -16,8 +16,8 @@ AC_DEFUN([DNSDIST_WITH_LIBSSL], [
         save_CFLAGS=$CFLAGS
         save_LIBS=$LIBS
         CFLAGS="$LIBSSL_CFLAGS $CFLAGS"
-        LIBS="$LIBSSL_LIBS $LIBS"
-        AC_CHECK_FUNCS([SSL_CTX_set_ciphersuites])
+        LIBS="$LIBSSL_LIBS -lcrypto $LIBS"
+        AC_CHECK_FUNCS([SSL_CTX_set_ciphersuites OCSP_basic_sign])
         CFLAGS=$save_CFLAGS
         LIBS=$save_LIBS
 
index 5e506e24d5ec293582b3a63cbce0812297f9e914..fab7815debae23ce193b09dbe62b55d8b1418a69 100644 (file)
@@ -422,6 +422,7 @@ public:
       SSL_CTX_sess_set_cache_size(d_tlsCtx.get(), fe.d_maxStoredSessions);
     }
 
+    std::vector<int> 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,29 @@ 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 */
+      auto keyType = libssl_get_last_key_type(d_tlsCtx);
+      if (keyType < 0) {
+        throw std::runtime_error("Key from '" + pair.second + "' has an unknown type");
+      }
+      keyTypes.push_back(keyType);
+    }
+
+    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 +536,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<std::map<int, std::string>*>(arg);
+    return libssl_ocsp_stapling_callback(ssl, *ocspMap);
+  }
+
   std::unique_ptr<TLSConnection> getConnection(int socket, unsigned int timeout, time_t now) override
   {
     handleTicketsKeyRotation(now);
@@ -562,6 +595,7 @@ public:
 
 private:
   OpenSSLTLSTicketKeysRing d_ticketKeys;
+  std::map<int, std::string> d_ocspResponses;
   std::unique_ptr<SSL_CTX, void(*)(SSL_CTX*)> d_tlsCtx;
   static std::atomic<uint64_t> s_users;
 };
@@ -918,6 +952,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) {
index 9af3239f466136dd2e584d7efdbbd257974d4cbf..fc8b539b6ed99e60e54348ffdacc8b98654f05ae 100644 (file)
@@ -7,6 +7,7 @@ struct DOHFrontend
 {
   std::shared_ptr<DOHServerConfig> d_dsc{nullptr};
   std::vector<std::pair<std::string, std::string>> d_certKeyPairs;
+  std::vector<std::string> d_ocspFiles;
   std::string d_ciphers;
   std::string d_ciphers13;
   std::string d_serverTokens{"h2o/dnsdist"};
index 3e7f52b081553bc239ff9628f4e608e6692d7b68..1718c664ce0a172176b359c5544edf8adf74b626 100644 (file)
@@ -139,6 +139,7 @@ public:
   }
 
   std::vector<std::pair<std::string, std::string>> d_certKeyPairs;
+  std::vector<std::string> d_ocspFiles;
   ComboAddress d_addr;
   std::string d_ciphers;
   std::string d_ciphers13;
index c970bcaf655aee4855dda45e103e84bcbaac611a..f50703bb2108405309c88cfe271f19d754f8911b 100644 (file)
@@ -14,4 +14,5 @@
 /server.csr
 /server.key
 /server.pem
+/server.ocsp
 /configs
\ No newline at end of file
index 1f6de2ea1c0f1504063781cec1186d8c742814a9..eb2fc9b4c174382c0e9f5b4aa13884df6abdaa7d 100755 (executable)
@@ -46,7 +46,7 @@ if [ "${PDNS_DEBUG}" = "YES" ]; then
   set -x
 fi
 
-rm -f ca.key ca.pem ca.srl server.csr server.key server.pem server.chain
+rm -f ca.key ca.pem ca.srl server.csr server.key server.pem server.chain server.ocsp
 rm -rf configs/*
 
 # Generate a new CA
@@ -66,4 +66,4 @@ if ! nosetests --with-xunit $@; then
     false
 fi
 
-rm ca.key ca.pem ca.srl server.csr server.key server.pem server.chain
+rm -f ca.key ca.pem ca.srl server.csr server.key server.pem server.chain server.ocsp
diff --git a/regression-tests.dnsdist/test_OCSP.py b/regression-tests.dnsdist/test_OCSP.py
new file mode 100644 (file)
index 0000000..d3a4ed3
--- /dev/null
@@ -0,0 +1,93 @@
+#!/usr/bin/env python
+import dns
+import subprocess
+from dnsdisttests import DNSDistTest
+
+class DNSDistOCSPStaplingTest(DNSDistTest):
+
+    @classmethod
+    def checkOCSPStaplingStatus(cls, addr, port, serverName, caFile):
+        testcmd = ['openssl', 's_client', '-CAfile', caFile, '-connect', '%s:%d' % (addr, port), '-status', '-servername', serverName ]
+        output = None
+        try:
+            process = subprocess.Popen(testcmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True)
+            output = process.communicate(input='')
+        except subprocess.CalledProcessError as exc:
+            raise AssertionError('dnsdist --check-config failed (%d): %s' % (exc.returncode, exc.output))
+
+        return output[0].decode()
+
+class TestOCSPStaplingDOH(DNSDistOCSPStaplingTest):
+
+    _serverKey = 'server.key'
+    _serverCert = 'server.chain'
+    _serverName = 'tls.tests.dnsdist.org'
+    _ocspFile = 'server.ocsp'
+    _caCert = 'ca.pem'
+    _caKey = 'ca.key'
+    _dohServerPort = 8443
+    _config_template = """
+    newServer{address="127.0.0.1:%s"}
+
+    -- generate an OCSP response file for our certificate, valid one day
+    generateOCSPResponse('%s', '%s', '%s', '%s', 1, 0)
+    addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" }, { ocspResponses={"%s"}})
+    """
+    _config_params = ['_testServerPort', '_serverCert', '_caCert', '_caKey', '_ocspFile', '_dohServerPort', '_serverCert', '_serverKey', '_ocspFile']
+
+    def testOCSPStapling(self):
+        """
+        OCSP Stapling: DOH
+        """
+        output = self.checkOCSPStaplingStatus('127.0.0.1', self._dohServerPort, self._serverName, self._caCert)
+        self.assertIn('OCSP Response Status: successful (0x0)', output)
+
+class TestOCSPStaplingTLSGnuTLS(DNSDistOCSPStaplingTest):
+
+    _serverKey = 'server.key'
+    _serverCert = 'server.chain'
+    _serverName = 'tls.tests.dnsdist.org'
+    _ocspFile = 'server.ocsp'
+    _caCert = 'ca.pem'
+    _caKey = 'ca.key'
+    _tlsServerPort = 8443
+    _config_template = """
+    newServer{address="127.0.0.1:%s"}
+
+    -- generate an OCSP response file for our certificate, valid one day
+    generateOCSPResponse('%s', '%s', '%s', '%s', 1, 0)
+    addTLSLocal("127.0.0.1:%s", "%s", "%s", { provider="gnutls", ocspResponses={"%s"}})
+    """
+    _config_params = ['_testServerPort', '_serverCert', '_caCert', '_caKey', '_ocspFile', '_tlsServerPort', '_serverCert', '_serverKey', '_ocspFile']
+
+    def testOCSPStapling(self):
+        """
+        OCSP Stapling: TLS (GnuTLS)
+        """
+        output = self.checkOCSPStaplingStatus('127.0.0.1', self._tlsServerPort, self._serverName, self._caCert)
+        self.assertIn('OCSP Response Status: successful (0x0)', output)
+
+class TestOCSPStaplingTLSOpenSSL(DNSDistOCSPStaplingTest):
+
+    _serverKey = 'server.key'
+    _serverCert = 'server.chain'
+    _serverName = 'tls.tests.dnsdist.org'
+    _ocspFile = 'server.ocsp'
+    _caCert = 'ca.pem'
+    _caKey = 'ca.key'
+    _tlsServerPort = 8443
+    _config_template = """
+    newServer{address="127.0.0.1:%s"}
+
+    -- generate an OCSP response file for our certificate, valid one day
+    generateOCSPResponse('%s', '%s', '%s', '%s', 1, 0)
+    addTLSLocal("127.0.0.1:%s", "%s", "%s", { provider="openssl", ocspResponses={"%s"}})
+    """
+    _config_params = ['_testServerPort', '_serverCert', '_caCert', '_caKey', '_ocspFile', '_tlsServerPort', '_serverCert', '_serverKey', '_ocspFile']
+
+    def testOCSPStapling(self):
+        """
+        OCSP Stapling: TLS (OpenSSL)
+        """
+        output = self.checkOCSPStaplingStatus('127.0.0.1', self._tlsServerPort, self._serverName, self._caCert)
+        self.assertIn('OCSP Response Status: successful (0x0)', output)