]> granicus.if.org Git - pdns/commitdiff
dnsdist: Add OCSP stapling (from files) for DoT and DoH
authorRemi Gacogne <remi.gacogne@powerdns.com>
Wed, 24 Jul 2019 17:04:09 +0000 (19:04 +0200)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Thu, 1 Aug 2019 07:45:02 +0000 (09:45 +0200)
The OCSP responses are loaded from files on startup, and reloaded
when the reloadAllCertificates() command is issued.

pdns/dnsdist-lua.cc
pdns/dnsdistdist/docs/reference/config.rst
pdns/dnsdistdist/doh.cc
pdns/dnsdistdist/libssl.cc
pdns/dnsdistdist/libssl.hh
pdns/dnsdistdist/tcpiohandler.cc
pdns/doh.hh
pdns/tcpiohandler.hh

index 6a0e5211dea19baa2272cbb7a39be014d1b5d950..4195d428ba70b6961e9fe2a228eef6ea5df471f4 100644 (file)
@@ -87,7 +87,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::map<std::string,std::string>, std::vector<std::pair<int, 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 +1732,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 +1894,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 {
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 f932c49f373f855e59f63d8c5afc68497196df06..bb4b6da94fb8b1ff04eb46fc6f129b9d51198064 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};
@@ -728,7 +730,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);
 
@@ -746,6 +757,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) {
@@ -756,6 +768,24 @@ 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 */
+    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();
index 684a276c314b05de11411c6dd78ddba9ae16f575..ce7e2660b89c9a16ed75f224dcaf07e7c8722231 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,131 @@ 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);
+}
+
 #endif /* HAVE_LIBSSL */
index a735815d542ad6313d9f6483b21897d6569f2ee5..0853400a693268d0aa85b8d30d0379873ab46621 100644 (file)
@@ -1,4 +1,21 @@
 #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);
+
+#endif /* HAVE_LIBSSL */
index 5e506e24d5ec293582b3a63cbce0812297f9e914..f14c6e8ddfecafbd4f09bc9ab88419c6c90ccb11 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,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<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 +591,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 +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) {
index b7b9bfe89fdf063ca6d69d11a4ce4a1925c594b6..3ff13ed6b58cf702685a1bb5d1024250dc031234 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;