From: Remi Gacogne Date: Mon, 20 Jan 2020 18:24:13 +0000 (+0100) Subject: rec: Add unit tests for the NSEC3 Opt-Out case X-Git-Tag: rec-4.3.0-rc1~2^2~2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=1f47c4bc47fbd5d25817eb40bc9396526d34840c;p=thirdparty%2Fpdns.git rec: Add unit tests for the NSEC3 Opt-Out case An Opt-Out NSEC3 only proves that there is no delegation, so we should not consider a DS NODATA or a NXDOMAIN proved by that RR secure but insecure. This was fixed in 18c8faae6c67f734583c5c881d0d083d3253b49e and this commit adds a few unit tests to cover the fix. (cherry picked from commit c179741988b9273b12c15a6b318ff0a43fe50081) --- diff --git a/pdns/recursordist/test-syncres_cc.cc b/pdns/recursordist/test-syncres_cc.cc index 5c116467c9..f4b9796b01 100644 --- a/pdns/recursordist/test-syncres_cc.cc +++ b/pdns/recursordist/test-syncres_cc.cc @@ -361,11 +361,11 @@ void addNSECRecordToLW(const DNSName& domain, const DNSName& next, const std::se records.push_back(rec); } -void addNSEC3RecordToLW(const DNSName& hashedName, const std::string& hashedNext, const std::string& salt, unsigned int iterations, const std::set& types, uint32_t ttl, std::vector& records) +void addNSEC3RecordToLW(const DNSName& hashedName, const std::string& hashedNext, const std::string& salt, unsigned int iterations, const std::set& types, uint32_t ttl, std::vector& records, bool optOut) { NSEC3RecordContent nrc; nrc.d_algorithm = 1; - nrc.d_flags = 0; + nrc.d_flags = optOut ? 1 : 0; nrc.d_iterations = iterations; nrc.d_salt = salt; nrc.d_nexthash = hashedNext; @@ -383,15 +383,15 @@ void addNSEC3RecordToLW(const DNSName& hashedName, const std::string& hashedNext records.push_back(rec); } -void addNSEC3UnhashedRecordToLW(const DNSName& domain, const DNSName& zone, const std::string& next, const std::set& types, uint32_t ttl, std::vector& records, unsigned int iterations) +void addNSEC3UnhashedRecordToLW(const DNSName& domain, const DNSName& zone, const std::string& next, const std::set& types, uint32_t ttl, std::vector& records, unsigned int iterations, bool optOut) { static const std::string salt = "deadbeef"; std::string hashed = hashQNameWithSalt(salt, iterations, domain); - addNSEC3RecordToLW(DNSName(toBase32Hex(hashed)) + zone, next, salt, iterations, types, ttl, records); + addNSEC3RecordToLW(DNSName(toBase32Hex(hashed)) + zone, next, salt, iterations, types, ttl, records, optOut); } -void addNSEC3NarrowRecordToLW(const DNSName& domain, const DNSName& zone, const std::set& types, uint32_t ttl, std::vector& records, unsigned int iterations) +void addNSEC3NarrowRecordToLW(const DNSName& domain, const DNSName& zone, const std::set& types, uint32_t ttl, std::vector& records, unsigned int iterations, bool optOut) { static const std::string salt = "deadbeef"; std::string hashed = hashQNameWithSalt(salt, iterations, domain); @@ -399,7 +399,7 @@ void addNSEC3NarrowRecordToLW(const DNSName& domain, const DNSName& zone, const incrementHash(hashedNext); decrementHash(hashed); - addNSEC3RecordToLW(DNSName(toBase32Hex(hashed)) + zone, hashedNext, salt, iterations, types, ttl, records); + addNSEC3RecordToLW(DNSName(toBase32Hex(hashed)) + zone, hashedNext, salt, iterations, types, ttl, records, optOut); } void generateKeyMaterial(const DNSName& name, unsigned int algo, uint8_t digest, testkeysset_t& keys) @@ -419,7 +419,7 @@ void generateKeyMaterial(const DNSName& name, unsigned int algo, uint8_t digest, dsAnchors[name].insert(keys[name].second); } -int genericDSAndDNSKEYHandler(LWResult* res, const DNSName& domain, DNSName auth, int type, const testkeysset_t& keys, bool proveCut, boost::optional now) +int genericDSAndDNSKEYHandler(LWResult* res, const DNSName& domain, DNSName auth, int type, const testkeysset_t& keys, bool proveCut, boost::optional now, bool nsec3, bool optOut) { if (type == QType::DS) { auth.chopOff(); @@ -438,12 +438,18 @@ int genericDSAndDNSKEYHandler(LWResult* res, const DNSName& domain, DNSName auth /* sign the SOA */ addRRSIG(keys, res->d_records, auth, 300, false, boost::none, boost::none, now); /* add a NSEC denying the DS */ - std::set types = {QType::NSEC}; + std::set types = { nsec3 ? QType::NSEC : QType::NSEC3 }; if (proveCut) { types.insert(QType::NS); } - addNSECRecordToLW(domain, DNSName("z") + domain, types, 600, res->d_records); + if (!nsec3) { + addNSECRecordToLW(domain, DNSName("z") + domain, types, 600, res->d_records); + } + else { + addNSEC3UnhashedRecordToLW(domain, auth, (DNSName("z") + domain).toString(), types, 600, res->d_records, 10, optOut); + } + addRRSIG(keys, res->d_records, auth, 300, false, boost::none, boost::none, now); } } diff --git a/pdns/recursordist/test-syncres_cc.hh b/pdns/recursordist/test-syncres_cc.hh index 0976c1389d..2b909b242f 100644 --- a/pdns/recursordist/test-syncres_cc.hh +++ b/pdns/recursordist/test-syncres_cc.hh @@ -61,16 +61,16 @@ bool addDS(const DNSName& domain, uint32_t ttl, std::vector& records, void addNSECRecordToLW(const DNSName& domain, const DNSName& next, const std::set& types, uint32_t ttl, std::vector& records); -void addNSEC3RecordToLW(const DNSName& hashedName, const std::string& hashedNext, const std::string& salt, unsigned int iterations, const std::set& types, uint32_t ttl, std::vector& records); +void addNSEC3RecordToLW(const DNSName& hashedName, const std::string& hashedNext, const std::string& salt, unsigned int iterations, const std::set& types, uint32_t ttl, std::vector& records, bool optOut = false); -void addNSEC3UnhashedRecordToLW(const DNSName& domain, const DNSName& zone, const std::string& next, const std::set& types, uint32_t ttl, std::vector& records, unsigned int iterations = 10); +void addNSEC3UnhashedRecordToLW(const DNSName& domain, const DNSName& zone, const std::string& next, const std::set& types, uint32_t ttl, std::vector& records, unsigned int iterations = 10, bool optOut = false); -void addNSEC3NarrowRecordToLW(const DNSName& domain, const DNSName& zone, const std::set& types, uint32_t ttl, std::vector& records, unsigned int iterations = 10); +void addNSEC3NarrowRecordToLW(const DNSName& domain, const DNSName& zone, const std::set& types, uint32_t ttl, std::vector& records, unsigned int iterations = 10, bool OptOut = false); void generateKeyMaterial(const DNSName& name, unsigned int algo, uint8_t digest, testkeysset_t& keys); void generateKeyMaterial(const DNSName& name, unsigned int algo, uint8_t digest, testkeysset_t& keys, map& dsAnchors); -int genericDSAndDNSKEYHandler(LWResult* res, const DNSName& domain, DNSName auth, int type, const testkeysset_t& keys, bool proveCut = true, boost::optional now = boost::none); +int genericDSAndDNSKEYHandler(LWResult* res, const DNSName& domain, DNSName auth, int type, const testkeysset_t& keys, bool proveCut = true, boost::optional now = boost::none, bool nsec3 = false, bool optOut = false); int basicRecordsForQnameMinimization(LWResult* res, const DNSName& domain, int type); diff --git a/pdns/recursordist/test-syncres_cc6.cc b/pdns/recursordist/test-syncres_cc6.cc index a056712935..df821755a8 100644 --- a/pdns/recursordist/test-syncres_cc6.cc +++ b/pdns/recursordist/test-syncres_cc6.cc @@ -746,6 +746,225 @@ BOOST_AUTO_TEST_CASE(test_dnssec_secure_to_insecure) BOOST_CHECK_EQUAL(queriesCount, 7U); } +BOOST_AUTO_TEST_CASE(test_dnssec_secure_to_insecure_optout) { + std::unique_ptr sr; + initSR(sr, true); + + setDNSSECValidation(sr, DNSSECMode::ValidateAll); + + primeHints(); + const DNSName target("powerdns.com."); + const ComboAddress targetAddr("192.0.2.42"); + testkeysset_t keys; + + auto luaconfsCopy = g_luaconfs.getCopy(); + luaconfsCopy.dsAnchors.clear(); + generateKeyMaterial(g_rootdnsname, DNSSECKeeper::ECDSA256, DNSSECKeeper::DIGEST_SHA256, keys, luaconfsCopy.dsAnchors); + generateKeyMaterial(DNSName("com."), DNSSECKeeper::ECDSA256, DNSSECKeeper::DIGEST_SHA256, keys); + + g_luaconfs.setState(luaconfsCopy); + + size_t queriesCount = 0; + + sr->setAsyncCallback([target,targetAddr,&queriesCount,keys](const ComboAddress& ip, const DNSName& domain, int type, bool doTCP, bool sendRDQuery, int EDNS0Level, struct timeval* now, boost::optional& srcmask, boost::optional context, LWResult* res, bool* chained) { + queriesCount++; + + if (type == QType::DS) { + if (domain == target) { + setLWResult(res, 0, true, false, true); + addRecordToLW(res, domain, QType::SOA, "pdns-public-ns1.powerdns.com. pieter\\.lexis.powerdns.com. 2017032301 10800 3600 604800 3600", DNSResourceRecord::AUTHORITY, 3600); + addRRSIG(keys, res->d_records, DNSName("com."), 300); + /* closest encloser */ + addNSEC3UnhashedRecordToLW(DNSName("com."), DNSName("com."), DNSName("a.com.").toStringNoDot(), { QType::NS }, 600, res->d_records, 10, true); + addRRSIG(keys, res->d_records, DNSName("com."), 300); + /* next closer */ + addNSEC3UnhashedRecordToLW(DNSName("oowerdns.com."), DNSName("com."), DNSName("qowerdns.com.").toStringNoDot(), { QType::NS }, 600, res->d_records, 10, true); + addRRSIG(keys, res->d_records, DNSName("com."), 300); + return 1; + } else { + return genericDSAndDNSKEYHandler(res, domain, domain, type, keys, true, boost::none, true, true); + } + } + else if (type == QType::DNSKEY) { + if (domain == g_rootdnsname || domain == DNSName("com.")) { + setLWResult(res, 0, true, false, true); + addDNSKEY(keys, domain, 300, res->d_records); + addRRSIG(keys, res->d_records, domain, 300); + return 1; + } + else { + setLWResult(res, 0, true, false, true); + addRecordToLW(res, domain, QType::SOA, "pdns-public-ns1.powerdns.com. pieter\\.lexis.powerdns.com. 2017032301 10800 3600 604800 3600", DNSResourceRecord::AUTHORITY, 3600); + return 1; + } + } + else { + if (isRootServer(ip)) { + setLWResult(res, 0, false, false, true); + addRecordToLW(res, "com.", QType::NS, "a.gtld-servers.com.", DNSResourceRecord::AUTHORITY, 3600); + addDS(DNSName("com."), 300, res->d_records, keys); + addRRSIG(keys, res->d_records, DNSName("."), 300); + addRecordToLW(res, "a.gtld-servers.com.", QType::A, "192.0.2.1", DNSResourceRecord::ADDITIONAL, 3600); + return 1; + } + else if (ip == ComboAddress("192.0.2.1:53")) { + if (domain == DNSName("com.")) { + setLWResult(res, 0, true, false, true); + addRecordToLW(res, DNSName("com."), QType::NS, "a.gtld-servers.com."); + addRRSIG(keys, res->d_records, DNSName("com."), 300); + addRecordToLW(res, "a.gtld-servers.com.", QType::A, "192.0.2.1", DNSResourceRecord::ADDITIONAL, 3600); + } + else { + setLWResult(res, 0, false, false, true); + addRecordToLW(res, domain, QType::NS, "ns1.powerdns.com.", DNSResourceRecord::AUTHORITY, 3600); + /* no DS */ + /* closest encloser */ + addNSEC3UnhashedRecordToLW(DNSName("com."), DNSName("com."), DNSName("a.com.").toStringNoDot(), { QType::NS }, 600, res->d_records, 10, true); + addRRSIG(keys, res->d_records, DNSName("com."), 300); + /* next closer */ + addNSEC3UnhashedRecordToLW(DNSName("oowerdns.com."), DNSName("com."), DNSName("qowerdns.com.").toStringNoDot(), { QType::NS }, 600, res->d_records, 10, true); + addRRSIG(keys, res->d_records, DNSName("com."), 300); + addRecordToLW(res, "ns1.powerdns.com.", QType::A, "192.0.2.2", DNSResourceRecord::ADDITIONAL, 3600); + } + return 1; + } + else if (ip == ComboAddress("192.0.2.2:53")) { + setLWResult(res, 0, true, false, true); + if (type == QType::NS) { + addRecordToLW(res, domain, QType::NS, "ns1.powerdns.com."); + } + else { + addRecordToLW(res, domain, QType::A, targetAddr.toString()); + } + return 1; + } + } + + return 0; + }); + + vector ret; + int res = sr->beginResolve(target, QType(QType::DS), QClass::IN, ret); + BOOST_CHECK_EQUAL(res, RCode::NoError); + BOOST_CHECK_EQUAL(sr->getValidationState(), Insecure); + BOOST_REQUIRE_EQUAL(ret.size(), 6); + BOOST_CHECK(ret[0].d_type == QType::SOA); + BOOST_CHECK_EQUAL(queriesCount, 4); + + /* again, to test the cache */ + ret.clear(); + res = sr->beginResolve(target, QType(QType::DS), QClass::IN, ret); + BOOST_CHECK_EQUAL(res, RCode::NoError); + BOOST_CHECK_EQUAL(sr->getValidationState(), Insecure); + BOOST_REQUIRE_EQUAL(ret.size(), 6); + BOOST_CHECK(ret[0].d_type == QType::SOA); + BOOST_CHECK_EQUAL(queriesCount, 4); +} + +BOOST_AUTO_TEST_CASE(test_dnssec_secure_to_insecure_nxd_optout) { + std::unique_ptr sr; + initSR(sr, true); + + setDNSSECValidation(sr, DNSSECMode::ValidateAll); + + primeHints(); + const DNSName target("powerdns.com."); + const ComboAddress targetAddr("192.0.2.42"); + testkeysset_t keys; + + auto luaconfsCopy = g_luaconfs.getCopy(); + luaconfsCopy.dsAnchors.clear(); + generateKeyMaterial(g_rootdnsname, DNSSECKeeper::ECDSA256, DNSSECKeeper::DIGEST_SHA256, keys, luaconfsCopy.dsAnchors); + generateKeyMaterial(DNSName("com."), DNSSECKeeper::ECDSA256, DNSSECKeeper::DIGEST_SHA256, keys); + + g_luaconfs.setState(luaconfsCopy); + + size_t queriesCount = 0; + + sr->setAsyncCallback([target,targetAddr,&queriesCount,keys](const ComboAddress& ip, const DNSName& domain, int type, bool doTCP, bool sendRDQuery, int EDNS0Level, struct timeval* now, boost::optional& srcmask, boost::optional context, LWResult* res, bool* chained) { + queriesCount++; + + if (type == QType::DS) { + if (domain == target) { + setLWResult(res, RCode::NXDomain, true, false, true); + addRecordToLW(res, DNSName("com."), QType::SOA, "a.gtld-servers.com. hostmastercom. 2017032301 10800 3600 604800 3600", DNSResourceRecord::AUTHORITY, 3600); + addRRSIG(keys, res->d_records, DNSName("com."), 300); + /* closest encloser */ + addNSEC3UnhashedRecordToLW(DNSName("com."), DNSName("com."), DNSName("a.com.").toStringNoDot(), { QType::SOA, QType::NS, QType::DS, QType::DNSKEY, QType::RRSIG, QType::NSEC3PARAM }, 600, res->d_records, 10, true); + addRRSIG(keys, res->d_records, DNSName("com."), 300); + /* next closer */ + addNSEC3UnhashedRecordToLW(DNSName("oowerdns.com."), DNSName("com."), DNSName("qowerdns.com.").toStringNoDot(), { QType::NS }, 600, res->d_records, 10, true); + addRRSIG(keys, res->d_records, DNSName("com."), 300); + return 1; + } else { + return genericDSAndDNSKEYHandler(res, domain, domain, type, keys, true, boost::none, true, true); + } + } + else if (type == QType::DNSKEY) { + if (domain == g_rootdnsname || domain == DNSName("com.")) { + setLWResult(res, 0, true, false, true); + addDNSKEY(keys, domain, 300, res->d_records); + addRRSIG(keys, res->d_records, domain, 300); + return 1; + } + else { + setLWResult(res, 0, true, false, true); + addRecordToLW(res, DNSName("com."), QType::SOA, "a.gtld-servers.com. hostmastercom. 2017032301 10800 3600 604800 3600", DNSResourceRecord::AUTHORITY, 3600); + return 1; + } + } + else { + if (isRootServer(ip)) { + setLWResult(res, 0, false, false, true); + addRecordToLW(res, "com.", QType::NS, "a.gtld-servers.com.", DNSResourceRecord::AUTHORITY, 3600); + addDS(DNSName("com."), 300, res->d_records, keys); + addRRSIG(keys, res->d_records, DNSName("."), 300); + addRecordToLW(res, "a.gtld-servers.com.", QType::A, "192.0.2.1", DNSResourceRecord::ADDITIONAL, 3600); + return 1; + } + else if (ip == ComboAddress("192.0.2.1:53")) { + if (domain == DNSName("com.")) { + setLWResult(res, 0, true, false, true); + addRecordToLW(res, DNSName("com."), QType::NS, "a.gtld-servers.com."); + addRRSIG(keys, res->d_records, DNSName("com."), 300); + addRecordToLW(res, "a.gtld-servers.com.", QType::A, "192.0.2.1", DNSResourceRecord::ADDITIONAL, 3600); + } + else { + setLWResult(res, RCode::NXDomain, true, false, true); + addRecordToLW(res, DNSName("com."), QType::SOA, "a.gtld-servers.com. hostmastercom. 2017032301 10800 3600 604800 3600", DNSResourceRecord::AUTHORITY, 3600); + addRRSIG(keys, res->d_records, DNSName("com."), 300); + /* closest encloser */ + addNSEC3UnhashedRecordToLW(DNSName("com."), DNSName("com."), DNSName("a.com.").toStringNoDot(), { QType::SOA, QType::NS, QType::DS, QType::DNSKEY, QType::RRSIG, QType::NSEC3PARAM }, 600, res->d_records, 10, true); + addRRSIG(keys, res->d_records, DNSName("com."), 300); + /* next closer */ + addNSEC3UnhashedRecordToLW(DNSName("oowerdns.com."), DNSName("com."), DNSName("qowerdns.com.").toStringNoDot(), { QType::NS }, 600, res->d_records, 10, true); + addRRSIG(keys, res->d_records, DNSName("com."), 300); + } + return 1; + } + } + + return 0; + }); + + vector ret; + int res = sr->beginResolve(target, QType(QType::A), QClass::IN, ret); + BOOST_CHECK_EQUAL(res, RCode::NXDomain); + BOOST_CHECK_EQUAL(sr->getValidationState(), Insecure); + BOOST_REQUIRE_EQUAL(ret.size(), 6); + BOOST_CHECK(ret[0].d_type == QType::SOA); + BOOST_CHECK_EQUAL(queriesCount, 6); + + /* again, to test the cache */ + ret.clear(); + res = sr->beginResolve(target, QType(QType::A), QClass::IN, ret); + BOOST_CHECK_EQUAL(res, RCode::NXDomain); + BOOST_CHECK_EQUAL(sr->getValidationState(), Insecure); + BOOST_REQUIRE_EQUAL(ret.size(), 6); + BOOST_CHECK(ret[0].d_type == QType::SOA); + BOOST_CHECK_EQUAL(queriesCount, 6); +} + BOOST_AUTO_TEST_CASE(test_dnssec_secure_direct_ds) { /* diff --git a/pdns/validate.cc b/pdns/validate.cc index dcf5ff94d0..b8ce53a8d2 100644 --- a/pdns/validate.cc +++ b/pdns/validate.cc @@ -535,7 +535,7 @@ dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16 if (needWildcardProof) { /* We now need to look for a NSEC3 covering the closest (provable) encloser RFC 5155 section-7.2.1 - FRC 7129 section-5.5 + RFC 7129 section-5.5 */ LOG("Now looking for the closest encloser for "<= 1) {