From: Remi Gacogne Date: Mon, 22 Feb 2021 16:21:50 +0000 (+0100) Subject: rec: Check that the salt and iterations count match for NSEC3 entries X-Git-Tag: dnsdist-1.6.0-alpha2~12^2~13 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=684b0a56680ff0fb9ac4321ccba9966a1a54ce39;p=thirdparty%2Fpdns.git rec: Check that the salt and iterations count match for NSEC3 entries Otherwise we could end up using a hash computed with the wrong parameters, and thus not proving what we expected. --- diff --git a/pdns/recursordist/aggressive_nsec.cc b/pdns/recursordist/aggressive_nsec.cc index 0b0b74d4dc..053dd2c3a9 100644 --- a/pdns/recursordist/aggressive_nsec.cc +++ b/pdns/recursordist/aggressive_nsec.cc @@ -230,6 +230,7 @@ void AggressiveNSECCache::insertNSEC(const DNSName& zone, const DNSName& owner, { std::lock_guard lock(entry->d_lock); if (nsec3 && !entry->d_nsec3) { + d_entriesCount -= entry->d_entries.size(); entry->d_entries.clear(); entry->d_nsec3 = true; } @@ -276,8 +277,16 @@ void AggressiveNSECCache::insertNSEC(const DNSName& zone, const DNSName& owner, #warning Ponder storing everything in raw form, without the zone instead. It still needs to be a DNSName for NSEC, though next = DNSName(toBase32Hex(content->d_nexthash)) + zone; - entry->d_iterations = content->d_iterations; - entry->d_salt = content->d_salt; + + if (entry->d_iterations != content->d_iterations || entry->d_salt != content->d_salt) { + entry->d_iterations = content->d_iterations; + entry->d_salt = content->d_salt; + + // Clearing the existing entries since we can't use them, and it's likely a rollover + // If it instead is different servers using different parameters, well, too bad. + d_entriesCount -= entry->d_entries.size(); + entry->d_entries.clear(); + } } /* the TTL is already a TTD by now */ @@ -337,8 +346,6 @@ bool AggressiveNSECCache::getNSECBefore(time_t now, std::shared_ptrd_ttd <= now) { moveCacheItemToFront(zoneEntry->d_entries, it); - //idx.erase(it); - //--d_entriesCount; return false; } @@ -366,8 +373,6 @@ bool AggressiveNSECCache::getNSEC3(time_t now, std::shared_ptrd_entries.project(it); if (it->d_ttd <= now) { moveCacheItemToBack(zoneEntry->d_entries, firstIndexIterator); - //idx.erase(it); - //--d_entriesCount; return false; } @@ -490,8 +495,8 @@ bool AggressiveNSECCache::getNSEC3Denial(time_t now, std::shared_ptr(exactNSEC3.d_record); - if (!nsec3) { - LOG(" but the content is not valid"<d_iterations != iterations || nsec3->d_salt != salt) { + LOG(" but the content is not valid, or has a different salt or iterations count"<(closestNSEC3.d_record); + if (!nsec3 || nsec3->d_iterations != iterations || nsec3->d_salt != salt) { + LOG(" but the content is not valid, or has a different salt or iterations count"<(nextCloserEntry.d_record); + if (!nextCloserNsec3 || nextCloserNsec3->d_iterations != iterations || nextCloserNsec3->d_salt != salt) { + LOG("The NSEC3 covering the next closer is not valid, or has a different salt or iterations count, bailing out"<(wcEntry.d_record); - if (!nsec3) { - LOG(" but the content is not valid"<d_iterations != iterations || nsec3->d_salt != salt) { + LOG(" but the content is not valid, or has a different salt or iterations count"<(wcEntry.d_record); + if (!nsec3 || nsec3->d_iterations != iterations || nsec3->d_salt != salt) { + LOG("The content of the NSEC3 covering the wildcard is not valid, or has a different salt or iterations count"< soaSet; std::vector> soaSignatures; + /* we might not actually need the SOA if we find a matching wildcard, but let's not bother for now */ if (g_recCache->get(now, zoneEntry->d_zone, QType::SOA, true, &soaSet, who, false, routingTag, doDNSSEC ? &soaSignatures : nullptr, nullptr, nullptr, &cachedState) <= 0 || cachedState != vState::Secure) { LOG("No valid SOA found for "<d_zone<<", which is the best match for "<("NSEC3 5 3 10 20370101000000 20370101000000 24567 dummy. data"); cache->insertNSEC(DNSName("powerdns.org"), rec.d_name, rec, {rrsig}, true); @@ -934,4 +934,126 @@ BOOST_AUTO_TEST_CASE(test_aggressive_nsec_dump) free(line); } +BOOST_AUTO_TEST_CASE(test_aggressive_nsec3_rollover) +{ + /* test that we don't compare a hash using the wrong (former) salt or iterations count in case of a rollover, + or when different servers use different parameters */ + auto cache = make_unique(10000); + g_recCache = std::make_unique(); + + const DNSName zone("powerdns.com"); + time_t now = time(nullptr); + + /* first we need a SOA */ + std::vector records; + time_t ttd = now + 30; + DNSRecord drSOA; + drSOA.d_name = zone; + drSOA.d_type = QType::SOA; + drSOA.d_class = QClass::IN; + drSOA.d_content = std::make_shared("pdns-public-ns1.powerdns.com. pieter\\.lexis.powerdns.com. 2017032301 10800 3600 604800 3600"); + drSOA.d_ttl = static_cast(ttd); // XXX truncation + drSOA.d_place = DNSResourceRecord::ANSWER; + records.push_back(drSOA); + + g_recCache->replace(now, zone, QType(QType::SOA), records, {}, {}, true, zone, boost::none, boost::none, vState::Secure); + BOOST_CHECK_EQUAL(g_recCache->size(), 1U); + + std::string oldSalt = "ab"; + std::string newSalt = "cd"; + unsigned int oldIterationsCount = 2; + unsigned int newIterationsCount = 1; + DNSName name("www.powerdns.com"); + std::string hashed = hashQNameWithSalt(oldSalt, oldIterationsCount, name); + + DNSRecord rec; + rec.d_name = DNSName(toBase32Hex(hashed)) + zone; + rec.d_type = QType::NSEC3; + rec.d_ttl = now + 10; + + NSEC3RecordContent nrc; + nrc.d_algorithm = 1; + nrc.d_flags = 0; + nrc.d_iterations = oldIterationsCount; + nrc.d_salt = oldSalt; + nrc.d_nexthash = hashed; + incrementHash(nrc.d_nexthash); + for (const auto& type : { QType::A }) { + nrc.set(type); + } + + rec.d_content = std::make_shared(nrc); + auto rrsig = std::make_shared("NSEC3 5 3 10 20370101000000 20370101000000 24567 dummy. data"); + cache->insertNSEC(zone, rec.d_name, rec, {rrsig}, true); + + BOOST_CHECK_EQUAL(cache->getEntriesCount(), 1); + + int res; + std::vector results; + + /* we can use the NSEC3s we have */ + /* direct match */ + BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), true); + + DNSName other("other.powerdns.com"); + /* now we insert a new NSEC3, with a different salt, changing that value for the zone */ + hashed = hashQNameWithSalt(newSalt, oldIterationsCount, other); + rec.d_name = DNSName(toBase32Hex(hashed)) + zone; + rec.d_type = QType::NSEC3; + rec.d_ttl = now + 10; + nrc.d_algorithm = 1; + nrc.d_flags = 0; + nrc.d_iterations = oldIterationsCount; + nrc.d_salt = newSalt; + nrc.d_nexthash = hashed; + incrementHash(nrc.d_nexthash); + for (const auto& type : { QType::A }) { + nrc.set(type); + } + + rec.d_content = std::make_shared(nrc); + rrsig = std::make_shared("NSEC3 5 3 10 20370101000000 20370101000000 24567 dummy. data"); + cache->insertNSEC(zone, rec.d_name, rec, {rrsig}, true); + + /* the existing entries should have been cleared */ + BOOST_CHECK_EQUAL(cache->getEntriesCount(), 1); + + /* we should be able to find a direct match for that name */ + /* direct match */ + BOOST_CHECK_EQUAL(cache->getDenial(now, other, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), true); + + /* but we should not be able to use the other NSEC3s */ + BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), false); + + /* and the same thing but this time updating the iterations count instead of the salt */ + DNSName other2("other2.powerdns.com"); + hashed = hashQNameWithSalt(newSalt, newIterationsCount, other2); + rec.d_name = DNSName(toBase32Hex(hashed)) + zone; + rec.d_type = QType::NSEC3; + rec.d_ttl = now + 10; + nrc.d_algorithm = 1; + nrc.d_flags = 0; + nrc.d_iterations = newIterationsCount; + nrc.d_salt = newSalt; + nrc.d_nexthash = hashed; + incrementHash(nrc.d_nexthash); + for (const auto& type : { QType::A }) { + nrc.set(type); + } + + rec.d_content = std::make_shared(nrc); + rrsig = std::make_shared("NSEC3 5 3 10 20370101000000 20370101000000 24567 dummy. data"); + cache->insertNSEC(zone, rec.d_name, rec, {rrsig}, true); + + /* the existing entries should have been cleared */ + BOOST_CHECK_EQUAL(cache->getEntriesCount(), 1); + + /* we should be able to find a direct match for that name */ + /* direct match */ + BOOST_CHECK_EQUAL(cache->getDenial(now, other2, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), true); + + /* but we should not be able to use the other NSEC3s */ + BOOST_CHECK_EQUAL(cache->getDenial(now, other, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), false); +} + BOOST_AUTO_TEST_SUITE_END()