From 8c15553e35940f3a3fcb5fbef53bca63c974150c Mon Sep 17 00:00:00 2001 From: Remi Gacogne Date: Thu, 8 Aug 2019 15:28:49 +0200 Subject: [PATCH] dnsdist: Add regression tests for OCSP stapling over DoH and DoT --- build-scripts/travis.sh | 2 +- pdns/dnsdist-lua.cc | 10 +++ pdns/dnsdistdist/libssl.cc | 47 +++++++++++ pdns/dnsdistdist/libssl.hh | 4 + pdns/dnsdistdist/m4/dnsdist_with_libssl.m4 | 4 +- regression-tests.dnsdist/.gitignore | 1 + regression-tests.dnsdist/runtests | 4 +- regression-tests.dnsdist/test_OCSP.py | 93 ++++++++++++++++++++++ 8 files changed, 160 insertions(+), 5 deletions(-) create mode 100644 regression-tests.dnsdist/test_OCSP.py diff --git a/build-scripts/travis.sh b/build-scripts/travis.sh index d4275eb590..99ca9043ed 100755 --- a/build-scripts/travis.sh +++ b/build-scripts/travis.sh @@ -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 .." } diff --git a/pdns/dnsdist-lua.cc b/pdns/dnsdist-lua.cc index 62a2341667..e42bed9ea9 100644 --- a/pdns/dnsdist-lua.cc +++ b/pdns/dnsdist-lua.cc @@ -45,6 +45,10 @@ #include "protobuf.hh" #include "sodcrypto.hh" +#ifdef HAVE_LIBSSL +#include "libssl.hh" +#endif + #include #include @@ -2039,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> setupLua(bool client, const std::string& config) diff --git a/pdns/dnsdistdist/libssl.cc b/pdns/dnsdistdist/libssl.cc index ce7e2660b8..d0a2da24b0 100644 --- a/pdns/dnsdistdist/libssl.cc +++ b/pdns/dnsdistdist/libssl.cc @@ -217,4 +217,51 @@ int libssl_get_last_key_type(std::unique_ptr& ctx) 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(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(PEM_read_X509_AUX(fp.get(), nullptr, nullptr, nullptr), X509_free); + + fp = std::unique_ptr(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(PEM_read_X509_AUX(fp.get(), nullptr, nullptr, nullptr), X509_free); + fp = std::unique_ptr(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(PEM_read_PrivateKey(fp.get(), nullptr, nullptr, nullptr), EVP_PKEY_free); + fp.reset(); + + auto bs = std::unique_ptr(OCSP_BASICRESP_new(), OCSP_BASICRESP_free); + auto thisupd = std::unique_ptr(X509_gmtime_adj(nullptr, 0), ASN1_TIME_free); + auto nextupd = std::unique_ptr(X509_time_adj_ex(nullptr, ndays, nmin * 60, nullptr), ASN1_TIME_free); + + auto cid = std::unique_ptr(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_create(OCSP_RESPONSE_STATUS_SUCCESSFUL, bs.get()), OCSP_RESPONSE_free); + auto bio = std::unique_ptr(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 */ diff --git a/pdns/dnsdistdist/libssl.hh b/pdns/dnsdistdist/libssl.hh index 0853400a69..f722e5cdde 100644 --- a/pdns/dnsdistdist/libssl.hh +++ b/pdns/dnsdistdist/libssl.hh @@ -18,4 +18,8 @@ int libssl_ocsp_stapling_callback(SSL* ssl, const std::map& oc std::map libssl_load_ocsp_responses(const std::vector& ocspFiles, std::vector keyTypes); int libssl_get_last_key_type(std::unique_ptr& 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 */ diff --git a/pdns/dnsdistdist/m4/dnsdist_with_libssl.m4 b/pdns/dnsdistdist/m4/dnsdist_with_libssl.m4 index 624a3d688f..730b33c7fb 100644 --- a/pdns/dnsdistdist/m4/dnsdist_with_libssl.m4 +++ b/pdns/dnsdistdist/m4/dnsdist_with_libssl.m4 @@ -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 diff --git a/regression-tests.dnsdist/.gitignore b/regression-tests.dnsdist/.gitignore index c970bcaf65..f50703bb21 100644 --- a/regression-tests.dnsdist/.gitignore +++ b/regression-tests.dnsdist/.gitignore @@ -14,4 +14,5 @@ /server.csr /server.key /server.pem +/server.ocsp /configs \ No newline at end of file diff --git a/regression-tests.dnsdist/runtests b/regression-tests.dnsdist/runtests index 1f6de2ea1c..eb2fc9b4c1 100755 --- a/regression-tests.dnsdist/runtests +++ b/regression-tests.dnsdist/runtests @@ -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 index 0000000000..d3a4ed3a84 --- /dev/null +++ b/regression-tests.dnsdist/test_OCSP.py @@ -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) -- 2.47.2