From: Pieter Lexis Date: Mon, 12 Apr 2021 10:58:56 +0000 (+0200) Subject: auth: Implement RFC 7872 and 9018 (COOKIES) X-Git-Tag: dnsdist-1.7.0-alpha1~3^2~13 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=37063755dd0973cb75396f0bf629e9d68b54b6f3;p=thirdparty%2Fpdns.git auth: Implement RFC 7872 and 9018 (COOKIES) This implements the siphash-based interoperable DNS COOKIES defined in RFC 9018 for the authoritative server. The EDNSCookieOpt struct has been expanded to accomodate this and can now has constructors and functions to check and generate a server cookie. Cookies will only be sent out if the client sent a cookie and the edns-cookie-secret setting is configures. The auth will respond with EDNS+FORMERR when the client cookie is malformed, BADCOOKIE when the client sent a server cookie we can't decode or is invalid and a normal response with a cookie (either new or sent by the client) when the cookie can be validated. --- diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index c4cf3345a5..1d9a6be31b 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -1515,6 +1515,7 @@ sigs SIGUSR singlethreaded Sipek +siphash sizeof Sjoerd slapd diff --git a/.not-formatted b/.not-formatted index 6a2ad40a94..8c3a8a86ff 100644 --- a/.not-formatted +++ b/.not-formatted @@ -155,7 +155,6 @@ ./pdns/dynloader.cc ./pdns/dynmessenger.cc ./pdns/dynmessenger.hh -./pdns/ednscookies.cc ./pdns/ednsoptions.cc ./pdns/ednsoptions.hh ./pdns/ednspadding.cc diff --git a/docs/settings.rst b/docs/settings.rst index effef87d12..eb6e8cdba5 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -622,6 +622,21 @@ ADDITIONAL section when sending a referral. Seconds to cache zone metadata from the database. A value of 0 disables caching. +.. _setting-edns-cookie-secret: + +``edns-cookie-secret`` +-------------------------- + +.. versionadded:: 4.6.0 + +- String +- Default: (empty) + +When set, PowerDNS will respond with :rfc:`9018` EDNS Cookies to queries that have the EDNS0 Cookie option. +PowerDNS will also respond with BADCOOKIE to clients that have no or a bad server cookie (section 5.2.3 and 5.2.4 of :rfc:`7873`). + +This setting MUST be 32 hexadecimal characters, as the siphash algorithm's key used to create the cookie requires a 128-bit key. + .. _setting-edns-subnet-processing: ``edns-subnet-processing`` diff --git a/modules/remotebackend/Makefile.am b/modules/remotebackend/Makefile.am index f1aaa95e0c..1b5fc20025 100644 --- a/modules/remotebackend/Makefile.am +++ b/modules/remotebackend/Makefile.am @@ -8,6 +8,10 @@ if LUA AM_CPPFLAGS +=$(LUA_CFLAGS) endif +if LIBSODIUM +AM_CPPFLAGS +=$(LIBSODIUM_CFLAGS) +endif + AM_LDFLAGS = $(THREADFLAGS) JSON11_LIBS = $(top_builddir)/ext/json11/libjson11.la @@ -114,6 +118,7 @@ libtestremotebackend_la_SOURCES = \ ../../pdns/dnsrecords.cc \ ../../pdns/dnssecinfra.cc \ ../../pdns/dnswriter.cc \ + ../../pdns/ednscookies.cc \ ../../pdns/ednsoptions.cc ../../pdns/ednsoptions.hh \ ../../pdns/ednssubnet.cc \ ../../pdns/iputils.cc \ @@ -150,6 +155,10 @@ libtestremotebackend_la_LDFLAGS = \ $(AM_LDFLAGS) \ $(BOOST_UNIT_TEST_FRAMEWORK_LDFLAGS) +if LIBSODIUM +libtestremotebackend_la_LIBADD += $(LIBSODIUM_LIBS) +endif + if REMOTEBACKEND_ZEROMQ libtestremotebackend_la_LIBADD += $(LIBZMQ_LIBS) endif diff --git a/pdns/Makefile.am b/pdns/Makefile.am index d8a7cafbe0..97cd1a281a 100644 --- a/pdns/Makefile.am +++ b/pdns/Makefile.am @@ -221,6 +221,7 @@ pdns_server_SOURCES = \ dynhandler.cc dynhandler.hh \ dynlistener.cc dynlistener.hh \ dynmessenger.hh \ + ednscookies.cc ednscookies.hh \ ednsoptions.cc ednsoptions.hh \ ednssubnet.cc ednssubnet.hh \ histogram.hh \ @@ -349,6 +350,7 @@ pdnsutil_SOURCES = \ dnssecsigner.cc \ dnswriter.cc dnswriter.hh \ dynlistener.cc \ + ednscookies.cc \ ednsoptions.cc ednsoptions.hh \ ednssubnet.cc \ ipcipher.cc ipcipher.hh \ @@ -1378,6 +1380,7 @@ testrunner_SOURCES = \ test-dnsrecordcontent.cc \ test-dnsrecords_cc.cc \ test-dnswriter_cc.cc \ + test-ednscookie_cc.cc \ test-ipcrypt_cc.cc \ test-iputils_hh.cc \ test-ixfr_cc.cc \ diff --git a/pdns/common_startup.cc b/pdns/common_startup.cc index fabd808cae..a7c8bb30ba 100644 --- a/pdns/common_startup.cc +++ b/pdns/common_startup.cc @@ -165,6 +165,8 @@ void declareArguments() ::arg().setSwitch("any-to-tcp","Answer ANY queries with tc=1, shunting to TCP")="yes"; ::arg().setSwitch("edns-subnet-processing","If we should act on EDNS Subnet options")="no"; + ::arg().set("edns-cookie-secret", "When set, set a server cookie in a response to a query with a Client cookie (in hex)")=""; + ::arg().setSwitch("webserver","Start a webserver for monitoring (api=yes also enables the HTTP listener)")="no"; ::arg().setSwitch("webserver-print-arguments","If the webserver should print arguments")="no"; ::arg().set("webserver-address","IP Address of webserver/API to listen on")="127.0.0.1"; @@ -572,6 +574,25 @@ void mainthread() g_proxyProtocolACL.toMasks(::arg()["proxy-protocol-from"]); g_proxyProtocolMaximumSize = ::arg().asNum("proxy-protocol-maximum-size"); + if (::arg()["edns-cookie-secret"].size() != 0) { + // User wants cookie processing +#ifdef HAVE_CRYPTO_SHORTHASH // we can do siphash-based cookies + DNSPacket::s_doEDNSCookieProcessing = true; + try { + if (::arg()["edns-cookie-secret"].size() != 32) { + throw std::range_error("wrong size (" + std::to_string(::arg()["edns-cookie-secret"].size()) + "), must be 32"); + } + DNSPacket::s_EDNSCookieKey = makeBytesFromHex(::arg()["edns-cookie-secret"]); + } catch(const std::range_error &e) { + g_log< DNSPacket::replyPacket() const r->d_wantsnsid = d_wantsnsid; r->d_dnssecOk = d_dnssecOk; r->d_eso = d_eso; + r->d_eco = d_eco; r->d_haveednssubnet = d_haveednssubnet; r->d_haveednssection = d_haveednssection; + r->d_haveednscookie = d_haveednscookie; r->d_ednsversion = 0; r->d_ednsrcode = 0; @@ -567,6 +584,7 @@ try d_havetsig = mdp.getTSIGPos(); d_haveednssubnet = false; d_haveednssection = false; + d_haveednscookie = false; if(getEDNSOpts(mdp, &edo)) { d_haveednssection=true; @@ -589,6 +607,10 @@ try d_haveednssubnet=true; } } + else if (s_doEDNSCookieProcessing && option.first == EDNSOptionCode::COOKIE) { + d_haveednscookie = true; + d_eco.makeFromString(option.second); + } else { // cerr<<"Have an option #"<first<<": "<second)< #include "iputils.hh" #include "ednssubnet.hh" +#include "ednscookies.hh" #include #include #include @@ -124,6 +125,9 @@ public: bool couldBeCached() const; //!< returns 0 if this query should bypass the packet cache bool hasEDNSSubnet() const; bool hasEDNS() const; + bool hasEDNSCookie() const; + bool hasWellFormedEDNSCookie() const; + bool hasValidEDNSCookie(); // Not const, some cookie params might be set uint8_t getEDNSVersion() const { return d_ednsversion; }; void setEDNSRcode(uint16_t extRCode) { @@ -167,6 +171,8 @@ public: static uint16_t s_udpTruncationThreshold; static bool s_doEDNSSubnetProcessing; + static bool s_doEDNSCookieProcessing; + static string s_EDNSCookieKey; private: void pasteQ(const char *question, int length); //!< set the question of this packet, useful for crafting replies @@ -179,6 +185,7 @@ private: std::unordered_set d_dedup; string d_rawpacket; // this is where everything lives 8 EDNSSubnetOpts d_eso; + EDNSCookiesOpt d_eco; int d_maxreplylen{0}; int d_socket{-1}; // 4 @@ -192,6 +199,7 @@ private: bool d_tsigtimersonly{false}; bool d_wantsnsid{false}; bool d_haveednssubnet{false}; + bool d_haveednscookie{false}; bool d_haveednssection{false}; bool d_isQuery; }; diff --git a/pdns/ednscookies.cc b/pdns/ednscookies.cc index 5e20819cef..5805ff7a8d 100644 --- a/pdns/ednscookies.cc +++ b/pdns/ednscookies.cc @@ -20,32 +20,147 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "ednscookies.hh" +#include "misc.hh" -bool getEDNSCookiesOptFromString(const string& option, EDNSCookiesOpt* eco) +#include "config.h" +#ifdef HAVE_CRYPTO_SHORTHASH +#include +#endif + +EDNSCookiesOpt::EDNSCookiesOpt(const std::string& option) { - return getEDNSCookiesOptFromString(option.c_str(), option.length(), eco); + getEDNSCookiesOptFromString(option.c_str(), option.length()); } -bool getEDNSCookiesOptFromString(const char* option, unsigned int len, EDNSCookiesOpt* eco) +EDNSCookiesOpt::EDNSCookiesOpt(const char* option, unsigned int len) { - if(len != 8 && len < 16) - return false; - eco->client = string(option, 8); - if (len > 8) { - eco->server = string(option + 8, len - 8); - } - return true; + getEDNSCookiesOptFromString(option, len); +} + +bool EDNSCookiesOpt::makeFromString(const std::string& option) +{ + getEDNSCookiesOptFromString(option.c_str(), option.length()); + return isWellFormed(); +} + +bool EDNSCookiesOpt::makeFromString(const char* option, unsigned int len) +{ + getEDNSCookiesOptFromString(option, len); + return isWellFormed(); } -string makeEDNSCookiesOptString(const EDNSCookiesOpt& eco) +string EDNSCookiesOpt::makeOptString() const { string ret; - if (eco.client.length() != 8) + if (client.length() != 8) return ret; - if (eco.server.length() != 0 && (eco.server.length() < 8 || eco.server.length() > 32)) + if (server.length() != 0 && (server.length() < 8 || server.length() > 32)) return ret; - ret.assign(eco.client); - if (eco.server.length() != 0) - ret.append(eco.server); + ret.assign(client); + if (server.length() != 0) + ret.append(server); return ret; } + +void EDNSCookiesOpt::getEDNSCookiesOptFromString(const char* option, unsigned int len) +{ + checked = false; + valid = false; + should_refresh = false; + client.clear(); + server.clear(); + if (len < 8) + return; + client = string(option, 8); + if (len > 8) { + server = string(option + 8, len - 8); + } +} + +bool EDNSCookiesOpt::isValid(const string& secret, const ComboAddress& source) +{ +#ifdef HAVE_CRYPTO_SHORTHASH + if (checked && valid) { + // Ignore the new check, we already validated it + // XXX this _might_ not be the best behaviour though... + return valid; + } + checked = true; + if (server.length() != 16 || client.length() != 8) { + return false; + } + if (server[0] != '\x01') { + // Version is not 1, can't verify + return false; + } + uint32_t ts; + memcpy(&ts, &server[4], sizeof(ts)); + ts = ntohl(ts); + uint32_t now = static_cast(time(nullptr)); + if (rfc1982LessThan(now + 300, ts) && rfc1982LessThan(ts + 3600, now)) { + return false; + } + if (rfc1982LessThan(ts + 1800, now)) { + // RFC 9018 section 4.3: + // The DNS server SHOULD generate a new Server Cookie at least if the + // received Server Cookie from the client is more than half an hour old + should_refresh = true; + } + if (secret.length() != crypto_shorthash_KEYBYTES) { + // XXX should we throw std::range_error here? + return false; + } + + string toHash = client + server.substr(0, 8) + source.toByteString(); + string hashResult; + hashResult.resize(8); + crypto_shorthash( + reinterpret_cast(&hashResult[0]), + reinterpret_cast(&toHash[0]), + toHash.length(), + reinterpret_cast(&secret[0])); + valid = (server.substr(8) == hashResult); + return valid; +#else + return false; +#endif +} + +bool EDNSCookiesOpt::makeServerCookie(const string& secret, const ComboAddress& source) +{ +#ifdef HAVE_CRYPTO_SHORTHASH + if (valid && !should_refresh) { + return true; + } + checked = false; + valid = false; + should_refresh = false; + + if (secret.length() != crypto_shorthash_KEYBYTES) { + return false; + } + + server.clear(); + server = "\x01"; // Version + server.resize(4, '\0'); // 3 reserved bytes + uint32_t now = htonl(static_cast(time(nullptr))); + server += string(reinterpret_cast(&now), 4); + server.resize(8); + + string toHash = client; + toHash += server; + toHash += source.toByteString(); + server.resize(16); + crypto_shorthash( + reinterpret_cast(&server[8]), + reinterpret_cast(&toHash[0]), + toHash.length(), + reinterpret_cast(&secret[0])); + checked = true; + valid = true; + should_refresh = false; + return true; +#else + return false; +#endif +} diff --git a/pdns/ednscookies.hh b/pdns/ednscookies.hh index 4e6f42e6cd..14d4653ead 100644 --- a/pdns/ednscookies.hh +++ b/pdns/ednscookies.hh @@ -21,13 +21,60 @@ */ #pragma once #include "namespaces.hh" +#include "iputils.hh" struct EDNSCookiesOpt { + EDNSCookiesOpt(){}; + EDNSCookiesOpt(const std::string& option); + EDNSCookiesOpt(const char* option, unsigned int len); + + bool makeFromString(const std::string& option); + bool makeFromString(const char* option, unsigned int len); + + size_t size() const + { + return server.size() + client.size(); + } + + bool isWellFormed() const + { + // RFC7873 section 5.2.2 + // In summary, valid cookie lengths are 8 and 16 to 40 inclusive. + return ( + client.size() == 8 && (server.size() == 0 || (server.size() >= 8 && server.size() <= 32))); + } + + bool isValid(const string& secret, const ComboAddress& source); + bool makeServerCookie(const string& secret, const ComboAddress& source); + string makeOptString() const; + string getServer() const + { + return server; + } + string getClient() const + { + return client; + } + +private: + // Whether or not we checked this cookie + bool checked{false}; + // Whether or not the cookie is valid + bool valid{false}; + // Whether or not he cookie will expire within 30 minutes + bool should_refresh{false}; + + // the client cookie string client; + // the server cookie string server; -}; -bool getEDNSCookiesOptFromString(const char* option, unsigned int len, EDNSCookiesOpt* eco); -bool getEDNSCookiesOptFromString(const string& option, EDNSCookiesOpt* eco); -string makeEDNSCookiesOptString(const EDNSCookiesOpt& eco); + // Checks if the server cookie is correct + // 1. Checks the sizes of the client and server cookie + // 2. checks if the timestamp is still good (now - 3600 < ts < now + 300) + // 3. Whether or not the hash is correct + bool check(const string& secret, const ComboAddress& source); + + void getEDNSCookiesOptFromString(const char* option, unsigned int len); +}; diff --git a/pdns/packethandler.cc b/pdns/packethandler.cc index 6b39f11bc8..8dd451153f 100644 --- a/pdns/packethandler.cc +++ b/pdns/packethandler.cc @@ -1290,12 +1290,26 @@ std::unique_ptr PacketHandler::doQuestion(DNSPacket& p) return nullptr; } - if (p.hasEDNS() && p.getEDNSVersion() > 0) { - r = p.replyPacket(); + if (p.hasEDNS()) { + if(p.getEDNSVersion() > 0) { + r = p.replyPacket(); - // PacketWriter::addOpt will take care of setting this correctly in the packet - r->setEDNSRcode(ERCode::BADVERS); - return r; + // PacketWriter::addOpt will take care of setting this correctly in the packet + r->setEDNSRcode(ERCode::BADVERS); + return r; + } + if (p.hasEDNSCookie()) { + if (!p.hasWellFormedEDNSCookie()) { + r = p.replyPacket(); + r->setRcode(RCode::FormErr); + return r; + } + if (!p.hasValidEDNSCookie()) { + r = p.replyPacket(); + r->setEDNSRcode(ERCode::BADCOOKIE); + return r; + } + } } if(p.d_havetsig) { diff --git a/pdns/recursordist/test-ednsoptions_cc.cc b/pdns/recursordist/test-ednsoptions_cc.cc index cb07d377d6..6d36f14e33 100644 --- a/pdns/recursordist/test-ednsoptions_cc.cc +++ b/pdns/recursordist/test-ednsoptions_cc.cc @@ -22,10 +22,8 @@ static void getRawQueryWithECSAndCookie(const DNSName& name, const Netmask& ecs, DNSPacketWriter pw(query, name, QType::A, QClass::IN, 0); pw.commit(); - EDNSCookiesOpt cookiesOpt; - cookiesOpt.client = clientCookie; - cookiesOpt.server = serverCookie; - string cookiesOptionStr = makeEDNSCookiesOptString(cookiesOpt); + EDNSCookiesOpt cookiesOpt(clientCookie + serverCookie); + string cookiesOptionStr = cookiesOpt.makeOptString(); EDNSSubnetOpts ecsOpts; ecsOpts.source = ecs; string origECSOptionStr = makeEDNSSubnetOptsString(ecsOpts); diff --git a/pdns/test-dnsdist_cc.cc b/pdns/test-dnsdist_cc.cc index 2d49ad2439..17944d0f50 100644 --- a/pdns/test-dnsdist_cc.cc +++ b/pdns/test-dnsdist_cc.cc @@ -1120,10 +1120,8 @@ BOOST_AUTO_TEST_CASE(removeECSWhenFirstOption) { EDNSSubnetOpts ecsOpts; ecsOpts.source = Netmask(origRemote, ECSSourcePrefixV6); string origECSOptionStr = makeEDNSSubnetOptsString(ecsOpts); - EDNSCookiesOpt cookiesOpt; - cookiesOpt.client = string("deadbeef"); - cookiesOpt.server = string("deadbeef"); - string cookiesOptionStr = makeEDNSCookiesOptString(cookiesOpt); + EDNSCookiesOpt cookiesOpt("deadbeefdeadbeef"); + string cookiesOptionStr = cookiesOpt.makeOptString(); GenericDNSPacketWriter::optvect_t opts; opts.push_back(make_pair(EDNSOptionCode::ECS, origECSOptionStr)); opts.push_back(make_pair(EDNSOptionCode::COOKIE, cookiesOptionStr)); @@ -1173,11 +1171,9 @@ BOOST_AUTO_TEST_CASE(removeECSWhenIntermediaryOption) { ecsOpts.source = Netmask(origRemote, ECSSourcePrefixV4); string origECSOptionStr = makeEDNSSubnetOptsString(ecsOpts); - EDNSCookiesOpt cookiesOpt; - cookiesOpt.client = string("deadbeef"); - cookiesOpt.server = string("deadbeef"); - string cookiesOptionStr1 = makeEDNSCookiesOptString(cookiesOpt); - string cookiesOptionStr2 = makeEDNSCookiesOptString(cookiesOpt); + EDNSCookiesOpt cookiesOpt("deadbeefdeadbeef"); + string cookiesOptionStr1 = cookiesOpt.makeOptString(); + string cookiesOptionStr2 = cookiesOpt.makeOptString(); GenericDNSPacketWriter::optvect_t opts; opts.push_back(make_pair(EDNSOptionCode::COOKIE, cookiesOptionStr1)); @@ -1225,10 +1221,8 @@ BOOST_AUTO_TEST_CASE(removeECSWhenLastOption) { pw.xfr32BitInt(0x01020304); pw.commit(); - EDNSCookiesOpt cookiesOpt; - cookiesOpt.client = string("deadbeef"); - cookiesOpt.server = string("deadbeef"); - string cookiesOptionStr = makeEDNSCookiesOptString(cookiesOpt); + EDNSCookiesOpt cookiesOpt("deadbeefdeadbeef"); + string cookiesOptionStr = cookiesOpt.makeOptString(); EDNSSubnetOpts ecsOpts; ecsOpts.source = Netmask(origRemote, ECSSourcePrefixV4); string origECSOptionStr = makeEDNSSubnetOptsString(ecsOpts); @@ -1313,10 +1307,8 @@ BOOST_AUTO_TEST_CASE(rewritingWithoutECSWhenFirstOption) { EDNSSubnetOpts ecsOpts; ecsOpts.source = Netmask(origRemote, ECSSourcePrefixV4); string origECSOptionStr = makeEDNSSubnetOptsString(ecsOpts); - EDNSCookiesOpt cookiesOpt; - cookiesOpt.client = string("deadbeef"); - cookiesOpt.server = string("deadbeef"); - string cookiesOptionStr = makeEDNSCookiesOptString(cookiesOpt); + EDNSCookiesOpt cookiesOpt("deadbeefdeadbeef"); + string cookiesOptionStr = cookiesOpt.makeOptString(); GenericDNSPacketWriter::optvect_t opts; opts.push_back(make_pair(EDNSOptionCode::ECS, origECSOptionStr)); opts.push_back(make_pair(EDNSOptionCode::COOKIE, cookiesOptionStr)); @@ -1355,11 +1347,9 @@ BOOST_AUTO_TEST_CASE(rewritingWithoutECSWhenIntermediaryOption) { EDNSSubnetOpts ecsOpts; ecsOpts.source = Netmask(origRemote, ECSSourcePrefixV4); string origECSOptionStr = makeEDNSSubnetOptsString(ecsOpts); - EDNSCookiesOpt cookiesOpt; - cookiesOpt.client = string("deadbeef"); - cookiesOpt.server = string("deadbeef"); - string cookiesOptionStr1 = makeEDNSCookiesOptString(cookiesOpt); - string cookiesOptionStr2 = makeEDNSCookiesOptString(cookiesOpt); + EDNSCookiesOpt cookiesOpt("deadbeefdeadbeef"); + string cookiesOptionStr1 = cookiesOpt.makeOptString(); + string cookiesOptionStr2 = cookiesOpt.makeOptString(); GenericDNSPacketWriter::optvect_t opts; opts.push_back(make_pair(EDNSOptionCode::COOKIE, cookiesOptionStr1)); opts.push_back(make_pair(EDNSOptionCode::ECS, origECSOptionStr)); @@ -1399,10 +1389,8 @@ BOOST_AUTO_TEST_CASE(rewritingWithoutECSWhenLastOption) { EDNSSubnetOpts ecsOpts; ecsOpts.source = Netmask(origRemote, ECSSourcePrefixV4); string origECSOptionStr = makeEDNSSubnetOptsString(ecsOpts); - EDNSCookiesOpt cookiesOpt; - cookiesOpt.client = string("deadbeef"); - cookiesOpt.server = string("deadbeef"); - string cookiesOptionStr = makeEDNSCookiesOptString(cookiesOpt); + EDNSCookiesOpt cookiesOpt("deadbeefdeadbeef"); + string cookiesOptionStr = cookiesOpt.makeOptString(); GenericDNSPacketWriter::optvect_t opts; opts.push_back(make_pair(EDNSOptionCode::COOKIE, cookiesOptionStr)); opts.push_back(make_pair(EDNSOptionCode::ECS, origECSOptionStr)); @@ -1467,10 +1455,8 @@ BOOST_AUTO_TEST_CASE(test_getEDNSZ) { EDNSSubnetOpts ecsOpts; ecsOpts.source = Netmask(ComboAddress("127.0.0.1"), ECSSourcePrefixV4); string origECSOptionStr = makeEDNSSubnetOptsString(ecsOpts); - EDNSCookiesOpt cookiesOpt; - cookiesOpt.client = string("deadbeef"); - cookiesOpt.server = string("deadbeef"); - string cookiesOptionStr = makeEDNSCookiesOptString(cookiesOpt); + EDNSCookiesOpt cookiesOpt("deadbeefdeadbeef"); + string cookiesOptionStr = cookiesOpt.makeOptString(); GenericDNSPacketWriter::optvect_t opts; opts.push_back(make_pair(EDNSOptionCode::COOKIE, cookiesOptionStr)); opts.push_back(make_pair(EDNSOptionCode::ECS, origECSOptionStr)); @@ -1565,10 +1551,8 @@ BOOST_AUTO_TEST_CASE(test_addEDNSToQueryTurnedResponse) { EDNSSubnetOpts ecsOpts; ecsOpts.source = Netmask(ComboAddress("127.0.0.1"), ECSSourcePrefixV4); string origECSOptionStr = makeEDNSSubnetOptsString(ecsOpts); - EDNSCookiesOpt cookiesOpt; - cookiesOpt.client = string("deadbeef"); - cookiesOpt.server = string("deadbeef"); - string cookiesOptionStr = makeEDNSCookiesOptString(cookiesOpt); + EDNSCookiesOpt cookiesOpt("deadbeefdeadbeef"); + string cookiesOptionStr = cookiesOpt.makeOptString(); GenericDNSPacketWriter::optvect_t opts; opts.push_back(make_pair(EDNSOptionCode::COOKIE, cookiesOptionStr)); opts.push_back(make_pair(EDNSOptionCode::ECS, origECSOptionStr)); @@ -1774,11 +1758,9 @@ BOOST_AUTO_TEST_CASE(test_isEDNSOptionInOpt) { const string ecsOptionStr = makeEDNSSubnetOptsString(ecsOpts); const size_t sizeOfECSContent = ecsOptionStr.size(); const size_t sizeOfECSOption = /* option code */ 2 + /* option length */ 2 + sizeOfECSContent; - EDNSCookiesOpt cookiesOpt; - cookiesOpt.client = string("deadbeef"); - cookiesOpt.server = string("deadbeef"); - const string cookiesOptionStr = makeEDNSCookiesOptString(cookiesOpt); - const size_t sizeOfCookieOption = /* option code */ 2 + /* option length */ 2 + cookiesOpt.client.size() + cookiesOpt.server.size(); + EDNSCookiesOpt cookiesOpt("deadbeefdeadbeef"); + string cookiesOptionStr = cookiesOpt.makeOptString(); + const size_t sizeOfCookieOption = /* option code */ 2 + /* option length */ 2 + cookiesOpt.size(); /* GenericDNSPacketWriter::optvect_t opts; opts.push_back(make_pair(EDNSOptionCode::COOKIE, cookiesOptionStr)); diff --git a/pdns/test-ednscookie_cc.cc b/pdns/test-ednscookie_cc.cc new file mode 100644 index 0000000000..f7d06535fa --- /dev/null +++ b/pdns/test-ednscookie_cc.cc @@ -0,0 +1,111 @@ +/* + * This file is part of PowerDNS or dnsdist. + * Copyright -- PowerDNS.COM B.V. and its contributors + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of version 2 of the GNU General Public License as + * published by the Free Software Foundation. + * + * In addition, for the avoidance of any doubt, permission is granted to + * link this program with OpenSSL and to (re)distribute the binaries + * produced as the result of such linking. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +#define BOOST_TEST_DYN_LINK +#define BOOST_TEST_NO_MAIN + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif +#include "ednscookies.hh" + +#include + +BOOST_AUTO_TEST_SUITE(test_ednscookie) +BOOST_AUTO_TEST_CASE(test_getEDNSCookiesOptFromString) +{ + string cookie(""); + EDNSCookiesOpt eco(cookie); + // Length 0 + BOOST_CHECK(!eco.isWellFormed()); + + // Too short + cookie = "\x12\x34\x56\x78\x90\xab\xcd"; + BOOST_CHECK(!eco.makeFromString(cookie)); + + // Correct length client cookie + cookie = "\x12\x34\x56\x78\x90\xab\xcd\xef"; + BOOST_CHECK(eco.makeFromString(cookie)); + + // Too short server cookie + cookie = "\x12\x34\x56\x78\x90\xab\xcd\xef\x01"; + BOOST_CHECK(!eco.makeFromString(cookie)); + + cookie = "\x12\x34\x56\x78\x90\xab\xcd\xef\x12\x34\x56\x78\x90\xab\xcd"; + BOOST_CHECK(!eco.makeFromString(cookie)); + + // Have server cookie of correct length + cookie = "\x12\x34\x56\x78\x90\xab\xcd\xef"; + cookie += cookie; // size 16 + BOOST_CHECK(eco.makeFromString(cookie)); + + cookie += cookie; // size 32 + BOOST_CHECK(eco.makeFromString(cookie)); + + cookie += "\x12\x34\x56\x78\x90\xab\xcd\xef"; // size 40 (the max) + BOOST_CHECK(eco.makeFromString(cookie)); + + // Cookie total size too long + cookie += "\x01"; + BOOST_CHECK(!eco.makeFromString(cookie)); +} + +BOOST_AUTO_TEST_CASE(test_ctor) +{ + string cookie(""); + auto eco = EDNSCookiesOpt(cookie); + BOOST_CHECK(!eco.isWellFormed()); + + eco = EDNSCookiesOpt("\x12\x34\x56\x78\x90\xab\xcd\xef"); + BOOST_CHECK(eco.isWellFormed()); + BOOST_CHECK_EQUAL(8, eco.makeOptString().length()); +} + +#ifdef HAVE_CRYPTO_SHORTHASH +BOOST_AUTO_TEST_CASE(test_createEDNSServerCookie) +{ + auto eco = EDNSCookiesOpt("\x12\x34\x56\x78\x90\xab\xcd\xef"); + ComboAddress remote("192.0.2.2"); + + BOOST_CHECK(eco.isWellFormed()); + + // wrong keysize (not 128 bits) + string secret = "blablablabla"; + BOOST_CHECK(!eco.makeServerCookie(secret, remote)); + BOOST_CHECK(eco.isWellFormed()); + BOOST_CHECK(!eco.isValid(secret, remote)); + + secret = "blablablablablab"; + BOOST_CHECK(eco.makeServerCookie(secret, remote)); + BOOST_CHECK(eco.isWellFormed()); + BOOST_CHECK(eco.isValid(secret, remote)); + + EDNSCookiesOpt eco2(eco.makeOptString()); + BOOST_CHECK(!eco2.isValid(secret, ComboAddress("192.0.2.1"))); + BOOST_CHECK(!eco2.isValid("blablablablabla1", remote)); + BOOST_CHECK(eco2.isValid(secret, remote)); + + // Check is we marked it as valid before + BOOST_CHECK(eco2.isValid("blablablablabla1", remote)); +} +#endif + +BOOST_AUTO_TEST_SUITE_END() diff --git a/pdns/test-packetcache_hh.cc b/pdns/test-packetcache_hh.cc index 6a55effcff..64c60e972e 100644 --- a/pdns/test-packetcache_hh.cc +++ b/pdns/test-packetcache_hh.cc @@ -164,10 +164,8 @@ BOOST_AUTO_TEST_CASE(test_PacketCacheAuthCollision) { opt.source = Netmask("192.0.2.1/32"); ednsOptions.clear(); ednsOptions.push_back(std::make_pair(EDNSOptionCode::ECS, makeEDNSSubnetOptsString(opt))); - EDNSCookiesOpt cookiesOpt; - cookiesOpt.client = string("deadbeef"); - cookiesOpt.server = string("deadbeef"); - ednsOptions.push_back(std::make_pair(EDNSOptionCode::COOKIE, makeEDNSCookiesOptString(cookiesOpt))); + EDNSCookiesOpt cookiesOpt(string("deadbeefdeadbeef")); + ednsOptions.push_back(std::make_pair(EDNSOptionCode::COOKIE, cookiesOpt.makeOptString())); pw1.addOpt(512, 0, EDNSOpts::DNSSECOK, ednsOptions); pw1.commit(); @@ -182,9 +180,8 @@ BOOST_AUTO_TEST_CASE(test_PacketCacheAuthCollision) { opt.source = Netmask("192.0.2.1/32"); ednsOptions.clear(); ednsOptions.push_back(std::make_pair(EDNSOptionCode::ECS, makeEDNSSubnetOptsString(opt))); - cookiesOpt.client = string("deadbeef"); - cookiesOpt.server = string("badc0fee"); - ednsOptions.push_back(std::make_pair(EDNSOptionCode::COOKIE, makeEDNSCookiesOptString(cookiesOpt))); + cookiesOpt.makeFromString(string("deadbeefbadc0fee")); + ednsOptions.push_back(std::make_pair(EDNSOptionCode::COOKIE, cookiesOpt.makeOptString())); pw2.addOpt(512, 0, EDNSOpts::DNSSECOK, ednsOptions); pw2.commit(); @@ -352,14 +349,8 @@ BOOST_AUTO_TEST_CASE(test_PacketCacheRecCollision) { opt.source = Netmask("192.0.2.1/32"); ednsOptions.clear(); ednsOptions.push_back(std::make_pair(EDNSOptionCode::ECS, makeEDNSSubnetOptsString(opt))); - EDNSCookiesOpt cookiesOpt; - cookiesOpt.client = string("deadbeef"); - cookiesOpt.server = string("deadbeef"); - cookiesOpt.server[4] = -20; - cookiesOpt.server[5] = -114; - cookiesOpt.server[6] = 0; - cookiesOpt.server[7] = 0; - ednsOptions.push_back(std::make_pair(EDNSOptionCode::COOKIE, makeEDNSCookiesOptString(cookiesOpt))); + EDNSCookiesOpt cookiesOpt(string("deadbeefdead\x11\xee\x00\x00").c_str(), 16); + ednsOptions.push_back(std::make_pair(EDNSOptionCode::COOKIE, cookiesOpt.makeOptString())); pw1.addOpt(512, 0, EDNSOpts::DNSSECOK, ednsOptions); pw1.commit(); @@ -374,13 +365,8 @@ BOOST_AUTO_TEST_CASE(test_PacketCacheRecCollision) { opt.source = Netmask("192.0.2.1/32"); ednsOptions.clear(); ednsOptions.push_back(std::make_pair(EDNSOptionCode::ECS, makeEDNSSubnetOptsString(opt))); - cookiesOpt.client = string("deadbeef"); - cookiesOpt.server = string("deadbeef"); - cookiesOpt.server[4] = 103; - cookiesOpt.server[5] = 68; - cookiesOpt.server[6] = 0; - cookiesOpt.server[7] = 0; - ednsOptions.push_back(std::make_pair(EDNSOptionCode::COOKIE, makeEDNSCookiesOptString(cookiesOpt))); + cookiesOpt.makeFromString(string("deadbeefdead\x67\x44\x00\x00").c_str(), 16); + ednsOptions.push_back(std::make_pair(EDNSOptionCode::COOKIE, cookiesOpt.makeOptString())); pw2.addOpt(512, 0, EDNSOpts::DNSSECOK, ednsOptions); pw2.commit(); diff --git a/regression-tests.auth-py/authtests.py b/regression-tests.auth-py/authtests.py index f9bbe80b43..2047e6d574 100644 --- a/regression-tests.auth-py/authtests.py +++ b/regression-tests.auth-py/authtests.py @@ -547,8 +547,12 @@ options { raise TypeError("rcode is neither a str nor int") if msg.rcode() != rcode: - msgRcode = dns.rcode.to_text(msg.rcode()) - wantedRcode = dns.rcode.to_text(rcode) + try: + msgRcode = dns.rcode.to_text(msg.rcode()) + wantedRcode = dns.rcode.to_text(rcode) + except AttributeError: + msgRcode = msg.rcode() + wantedRcode = rcode raise AssertionError("Rcode for %s is %s, expected %s." % (msg.question[0].to_text(), msgRcode, wantedRcode)) diff --git a/regression-tests.auth-py/cookiesoption.py b/regression-tests.auth-py/cookiesoption.py new file mode 120000 index 0000000000..11e592d1d8 --- /dev/null +++ b/regression-tests.auth-py/cookiesoption.py @@ -0,0 +1 @@ +../regression-tests.dnsdist/cookiesoption.py \ No newline at end of file diff --git a/regression-tests.auth-py/test_Cookies.py b/regression-tests.auth-py/test_Cookies.py new file mode 100644 index 0000000000..50bc75f93c --- /dev/null +++ b/regression-tests.auth-py/test_Cookies.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python +import dns + +from authtests import AuthTest + + +class TestEdnsCookies(AuthTest): + _config_template = """ +launch=bind +edns-cookie-secret=aabbccddeeff11223344556677889900 +""" + + _zones = { + 'example.org': """ +example.org. 3600 IN SOA {soa} +example.org. 3600 IN NS ns1.example.org. +example.org. 3600 IN NS ns2.example.org. +ns1.example.org. 3600 IN A 192.0.2.10 +ns2.example.org. 3600 IN A 192.0.2.11 + +www.example.org. 3600 IN A 192.0.2.5 + """, + } + + def sendAndExpectNoCookie(self, msg, rcode): + res = self.sendUDPQuery(msg) + self.assertRcodeEqual(res, rcode) + self.assertFalse(any([opt.otype == dns.edns.COOKIE for + opt in res.options])) + + def getCookieFromServer(self): + opts = [ + dns.edns.GenericOption(dns.edns.COOKIE, + b'\x22\x11\x33\x44\x55\x66\x77\x88')] + query = dns.message.make_query('www.example.org', 'A', options=opts) + res = self.sendUDPQuery(query) + self.assertRcodeEqual(res, 23) # BADCOOKIE + for opt in res.options: + if opt.otype == dns.edns.COOKIE: + return opt + self.fail() + + def testNoCookie(self): + query = dns.message.make_query('www.example.org', 'A', use_edns=0) + self.sendAndExpectNoCookie(query, dns.rcode.NOERROR) + + def testClientCookieTooShort(self): + opts = [dns.edns.GenericOption(dns.edns.COOKIE, b'\x22')] + query = dns.message.make_query('www.example.org', 'A', options=opts) + self.sendAndExpectNoCookie(query, dns.rcode.FORMERR) + + opts = [dns.edns.GenericOption(dns.edns.COOKIE, + b'\x22\x11\x33\x44\x55\x66\x77')] + query = dns.message.make_query('www.example.org', 'A', options=opts) + self.sendAndExpectNoCookie(query, dns.rcode.FORMERR) + + def testServerCookieTooShort(self): + opts = [ + dns.edns.GenericOption(dns.edns.COOKIE, + b'\x22\x11\x33\x44\x55\x66\x77\x88\x99')] + query = dns.message.make_query('www.example.org', 'A', options=opts) + self.sendAndExpectNoCookie(query, dns.rcode.FORMERR) + + opts = [ + dns.edns.GenericOption(dns.edns.COOKIE, + b'\x22\x11\x33\x44\x55\x66\x77\x88' + + b'\x22\x11\x33\x44\x55\x66\x77')] + query = dns.message.make_query('www.example.org', 'A', options=opts) + self.sendAndExpectNoCookie(query, dns.rcode.FORMERR) + + def testOnlyClientCookie(self): + opts = [ + dns.edns.GenericOption(dns.edns.COOKIE, + b'\x22\x11\x33\x44\x55\x66\x77\x88')] + query = dns.message.make_query('www.example.org', 'A', options=opts) + res = self.sendUDPQuery(query) + self.assertRcodeEqual(res, 23) # BADCOOKIE + self.assertTrue(any([opt.otype == dns.edns.COOKIE for + opt in res.options])) + + def testCorrectCookie(self): + opts = [self.getCookieFromServer()] + query = dns.message.make_query('www.example.org', 'A', options=opts) + res = self.sendUDPQuery(query) + self.assertRcodeEqual(res, dns.rcode.NOERROR) + + def testBrokenCookie(self): + data = self.getCookieFromServer().data + data = data.replace(b'\x11', b'\x12') + opts = [dns.edns.GenericOption(dns.edns.COOKIE, data)] + query = dns.message.make_query('www.example.org', 'A', options=opts) + res = self.sendUDPQuery(query) + self.assertRcodeEqual(res, 23) + for opt in res.options: + if opt.otype == dns.edns.COOKIE: + self.assertNotEqual(opt.data, opts[0].data) + return + self.fail() diff --git a/regression-tests.dnsdist/cookiesoption.py b/regression-tests.dnsdist/cookiesoption.py index d977e1030d..8e4016d27b 100644 --- a/regression-tests.dnsdist/cookiesoption.py +++ b/regression-tests.dnsdist/cookiesoption.py @@ -96,3 +96,5 @@ class CookiesOption(dns.edns.Option): dns.edns._type_to_class[0x000A] = CookiesOption + +dns.rcode.BADCOOKIE = 23