#include "validate.hh" #include "misc.hh" #include "dnssecinfra.hh" #include "dnsseckeeper.hh" #include "rec-lua-conf.hh" #include "base32.hh" #include "logger.hh" bool g_dnssecLOG{false}; time_t g_signatureInceptionSkew{0}; uint16_t g_maxNSEC3Iterations{0}; #define LOG(x) if(g_dnssecLOG) { g_log < > getByTag(const skeyset_t& keys, uint16_t tag, uint8_t algorithm) { vector> ret; for (const auto& key : keys) { if (!isAZoneKey(*key)) { LOG("Key for tag "<d_protocol == 3 && key->getTag() == tag && key->d_algorithm == algorithm) { ret.push_back(key); } } return ret; } bool isCoveredByNSEC3Hash(const std::string& h, const std::string& beginHash, const std::string& nextHash) { return ((beginHash < h && h < nextHash) || // no wrap BEGINNING --- HASH -- END (nextHash > h && beginHash > nextHash) || // wrap HASH --- END --- BEGINNING (nextHash < beginHash && beginHash < h) || // wrap other case END --- BEGINNING --- HASH (beginHash == nextHash && h != beginHash)); // "we have only 1 NSEC3 record, LOL!" } bool isCoveredByNSEC3Hash(const DNSName& h, const DNSName& beginHash, const DNSName& nextHash) { return ((beginHash.canonCompare(h) && h.canonCompare(nextHash)) || // no wrap BEGINNING --- HASH -- END (h.canonCompare(nextHash) && nextHash.canonCompare(beginHash)) || // wrap HASH --- END --- BEGINNING (nextHash.canonCompare(beginHash) && beginHash.canonCompare(h)) || // wrap other case END --- BEGINNING --- HASH (beginHash == nextHash && h != beginHash)); // "we have only 1 NSEC3 record, LOL!" } bool isCoveredByNSEC(const DNSName& name, const DNSName& begin, const DNSName& next) { return ((begin.canonCompare(name) && name.canonCompare(next)) || // no wrap BEGINNING --- NAME --- NEXT (name.canonCompare(next) && next.canonCompare(begin)) || // wrap NAME --- NEXT --- BEGINNING (next.canonCompare(begin) && begin.canonCompare(name)) || // wrap other case NEXT --- BEGINNING --- NAME (begin == next && name != begin)); // "we have only 1 NSEC record, LOL!" } static bool nsecProvesENT(const DNSName& name, const DNSName& begin, const DNSName& next) { /* if name is an ENT: - begin < name - next is a child of name */ return begin.canonCompare(name) && next != name && next.isPartOf(name); } using nsec3HashesCache = std::map, std::string>; static std::string getHashFromNSEC3(const DNSName& qname, const std::shared_ptr& nsec3, nsec3HashesCache& cache) { std::string result; if (g_maxNSEC3Iterations && nsec3->d_iterations > g_maxNSEC3Iterations) { return result; } auto key = std::make_tuple(qname, nsec3->d_salt, nsec3->d_iterations); auto it = cache.find(key); if (it != cache.end()) { return it->second; } result = hashQNameWithSalt(nsec3->d_salt, nsec3->d_iterations, qname); cache[key] = result; return result; } /* There is no delegation at this exact point if: - the name exists but the NS type is not set - the name does not exist One exception, if the name is covered by an opt-out NSEC3 it doesn't prove that an insecure delegation doesn't exist. */ bool denialProvesNoDelegation(const DNSName& zone, const std::vector& dsrecords) { nsec3HashesCache cache; for (const auto& record : dsrecords) { if (record.d_type == QType::NSEC) { const auto nsec = getRR(record); if (!nsec) { continue; } if (record.d_name == zone) { return !nsec->isSet(QType::NS); } if (isCoveredByNSEC(zone, record.d_name, nsec->d_next)) { return true; } } else if (record.d_type == QType::NSEC3) { const auto nsec3 = getRR(record); if (!nsec3) { continue; } const string h = getHashFromNSEC3(zone, nsec3, cache); if (h.empty()) { return false; } const string beginHash = fromBase32Hex(record.d_name.getRawLabels()[0]); if (beginHash == h) { return !nsec3->isSet(QType::NS); } if (isCoveredByNSEC3Hash(h, beginHash, nsec3->d_nexthash)) { return !(nsec3->isOptOut()); } } } return false; } /* RFC 4035 section-5.3.4: "If the number of labels in an RRset's owner name is greater than the Labels field of the covering RRSIG RR, then the RRset and its covering RRSIG RR were created as a result of wildcard expansion." */ bool isWildcardExpanded(unsigned int labelCount, const std::shared_ptr& sign) { if (sign && sign->d_labels < labelCount) { return true; } return false; } static bool isWildcardExpanded(const DNSName& owner, const std::vector >& signatures) { if (signatures.empty()) { return false; } const auto& sign = signatures.at(0); unsigned int labelsCount = owner.countLabels(); return isWildcardExpanded(labelsCount, sign); } bool isWildcardExpandedOntoItself(const DNSName& owner, unsigned int labelCount, const std::shared_ptr& sign) { if (owner.isWildcard() && (labelCount - 1) == sign->d_labels) { /* this is a wildcard alright, but it has not been expanded */ return true; } return false; } static bool isWildcardExpandedOntoItself(const DNSName& owner, const std::vector >& signatures) { if (signatures.empty()) { return false; } const auto& sign = signatures.at(0); unsigned int labelsCount = owner.countLabels(); return isWildcardExpandedOntoItself(owner, labelsCount, sign); } /* if this is a wildcard NSEC, the owner name has been modified to match the name. Make sure we use the original '*' form. */ DNSName getNSECOwnerName(const DNSName& initialOwner, const std::vector >& signatures) { DNSName result = initialOwner; if (signatures.empty()) { return result; } const auto& sign = signatures.at(0); unsigned int labelsCount = initialOwner.countLabels(); if (sign && sign->d_labels < labelsCount) { do { result.chopOff(); labelsCount--; } while (sign->d_labels < labelsCount); result = g_wildcarddnsname + result; } return result; } static bool isNSECAncestorDelegation(const DNSName& signer, const DNSName& owner, const std::shared_ptr& nsec) { return nsec->isSet(QType::NS) && !nsec->isSet(QType::SOA) && signer.countLabels() < owner.countLabels(); } bool isNSEC3AncestorDelegation(const DNSName& signer, const DNSName& owner, const std::shared_ptr& nsec3) { return nsec3->isSet(QType::NS) && !nsec3->isSet(QType::SOA) && signer.countLabels() < owner.countLabels(); } static bool provesNoDataWildCard(const DNSName& qname, const uint16_t qtype, const DNSName& closestEncloser, const cspmap_t& validrrsets) { const DNSName wildcard = g_wildcarddnsname + closestEncloser; LOG("Trying to prove that there is no data in wildcard for "<getZoneRepresentation()<(r); if (!nsec) { continue; } DNSName owner = getNSECOwnerName(v.first.first, v.second.signatures); if (owner != wildcard) { continue; } LOG("\tWildcard matches"); if (qtype == 0 || isTypeDenied(nsec, QType(qtype))) { LOG(" and proves that the type did not exist"<= commonWithNext.countLabels()) { return commonWithOwner; } return commonWithNext; } /* This function checks whether the non-existence of a wildcard covering qname|qtype is proven by the NSEC records in validrrsets. */ static bool provesNoWildCard(const DNSName& qname, const uint16_t qtype, const DNSName& closestEncloser, const cspmap_t & validrrsets) { LOG("Trying to prove that there is no wildcard for "<getZoneRepresentation()<(r); if (!nsec) { continue; } const DNSName owner = getNSECOwnerName(v.first.first, v.second.signatures); LOG("Comparing owner: "<isSet(QType::DNAME)) { /* rfc6672 section 5.3.2: DNAME Bit in NSEC Type Map In any negative response, the NSEC or NSEC3 [RFC5155] record type bitmap SHOULD be checked to see that there was no DNAME that could have been applied. If the DNAME bit in the type bitmap is set and the query name is a subdomain of the closest encloser that is asserted, then DNAME substitution should have been done, but the substitution has not been done as specified. */ LOG("\tThe qname is a subdomain of the NSEC and the DNAME bit is set"<d_next)) { LOG("\tWildcard is covered"<getZoneRepresentation()<(r); if (!nsec3) { continue; } const DNSName signer = getSigner(v.second.signatures); if (!v.first.first.isPartOf(signer)) { continue; } string h = getHashFromNSEC3(wildcard, nsec3, cache); if (h.empty()) { return false; } LOG("\tWildcard hash: "< "<d_nexthash)<d_nexthash)) { LOG("\tWildcard hash is covered"<& nsec, const std::vector>& signatures) { const DNSName signer = getSigner(signatures); if (!name.isPartOf(signer) || !nsecOwner.isPartOf(signer)) { return dState::INCONCLUSIVE; } const DNSName owner = getNSECOwnerName(nsecOwner, signatures); /* RFC 6840 section 4.1 "Clarifications on Nonexistence Proofs": Ancestor delegation NSEC or NSEC3 RRs MUST NOT be used to assume nonexistence of any RRs below that zone cut, which include all RRs at that (original) owner name other than DS RRs, and all RRs below that owner name regardless of type. */ if (name.isPartOf(owner) && isNSECAncestorDelegation(signer, owner, nsec)) { /* this is an "ancestor delegation" NSEC RR */ if (!(qtype == QType::DS && name == owner)) { LOG("An ancestor delegation NSEC RR can only deny the existence of a DS"<isSet(QType::DNAME)) { /* rfc6672 section 5.3.2: DNAME Bit in NSEC Type Map In any negative response, the NSEC or NSEC3 [RFC5155] record type bitmap SHOULD be checked to see that there was no DNAME that could have been applied. If the DNAME bit in the type bitmap is set and the query name is a subdomain of the closest encloser that is asserted, then DNAME substitution should have been done, but the substitution has not been done as specified. */ LOG("The DNAME bit is set and the query name is a subdomain of that NSEC"); return dState::NODENIAL; } if (isCoveredByNSEC(name, owner, nsec->d_next)) { LOG(name<<" is covered by ("<d_next<<") "); if (nsecProvesENT(name, owner, nsec->d_next)) { LOG("Denies existence of type "<getZoneRepresentation()<(r); if (!nsec) { continue; } const DNSName owner = getNSECOwnerName(v.first.first, v.second.signatures); const DNSName signer = getSigner(v.second.signatures); if (!v.first.first.isPartOf(signer) || !owner.isPartOf(signer) ) { continue; } /* RFC 6840 section 4.1 "Clarifications on Nonexistence Proofs": Ancestor delegation NSEC or NSEC3 RRs MUST NOT be used to assume nonexistence of any RRs below that zone cut, which include all RRs at that (original) owner name other than DS RRs, and all RRs below that owner name regardless of type. */ if (qname.isPartOf(owner) && isNSECAncestorDelegation(signer, owner, nsec)) { /* this is an "ancestor delegation" NSEC RR */ if (!(qtype == QType::DS && qname == owner)) { LOG("An ancestor delegation NSEC RR can only deny the existence of a DS"<isSet(QType::NS)) { LOG("However, no NS record exists at this level!"<d_next); if (provesNoWildCard(qname, qtype, closestEncloser, validrrsets)) { return dState::NXQTYPE; } LOG("But the existence of a wildcard is not denied for "<isSet(QType::DNAME)) { /* rfc6672 section 5.3.2: DNAME Bit in NSEC Type Map In any negative response, the NSEC or NSEC3 [RFC5155] record type bitmap SHOULD be checked to see that there was no DNAME that could have been applied. If the DNAME bit in the type bitmap is set and the query name is a subdomain of the closest encloser that is asserted, then DNAME substitution should have been done, but the substitution has not been done as specified. */ LOG("The DNAME bit is set and the query name is a subdomain of that NSEC"); return dState::NODENIAL; } /* check if the whole NAME is denied existing */ if (isCoveredByNSEC(qname, owner, nsec->d_next)) { LOG(qname<<" is covered by ("<d_next<<") "); if (nsecProvesENT(qname, owner, nsec->d_next)) { if (wantsNoDataProof) { /* if the name is an ENT and we received a NODATA answer, we are fine with a NSEC proving that the name does not exist. */ LOG("Denies existence of type "<d_next); if (wantsNoDataProof) { LOG("looking for NODATA proof"<isSet(qtype)<<", next: "<d_next<getZoneRepresentation()<(r); if (!nsec3) { continue; } if (v.second.signatures.empty()) { continue; } const DNSName signer = getSigner(v.second.signatures); if (!v.first.first.isPartOf(signer)) { LOG("Owner "<isSet(QType::NS)) { LOG("However, no NS record exists at this level!"<getZoneRepresentation()<(r); if (!nsec3) { continue; } const DNSName signer = getSigner(v.second.signatures); if (!v.first.first.isPartOf(signer)) { LOG("Owner "<isSet(QType::DNAME)) { /* rfc6672 section 5.3.2: DNAME Bit in NSEC Type Map In any negative response, the NSEC or NSEC3 [RFC5155] record type bitmap SHOULD be checked to see that there was no DNAME that could have been applied. If the DNAME bit in the type bitmap is set and the query name is a subdomain of the closest encloser that is asserted, then DNAME substitution should have been done, but the substitution has not been done as specified. */ LOG("\tThe closest encloser NSEC3 has the DNAME bit is set"< 0 && closestEncloserLabelsCount > wildcardLabelsCount) { closestEncloser.chopOff(); closestEncloserLabelsCount--; } } bool nextCloserFound = false; bool isOptOut = false; if (found == true) { /* now that we have found the closest (provable) encloser, we can construct the next closer (RFC7129 section-5.5) name and look for a NSEC3 RR covering it */ unsigned int labelIdx = qname.countLabels() - closestEncloser.countLabels(); if (labelIdx >= 1) { DNSName nextCloser(closestEncloser); nextCloser.prependRawLabel(qname.getRawLabel(labelIdx - 1)); LOG("Looking for a NSEC3 covering the next closer name "<getZoneRepresentation()<(r); if(!nsec3) continue; string h = getHashFromNSEC3(nextCloser, nsec3, cache); if (h.empty()) { return dState::INSECURE; } const DNSName signer = getSigner(v.second.signatures); if (!v.first.first.isPartOf(signer)) { LOG("Owner "< "<d_nexthash)<d_nexthash)) { LOG("Denies existence of name "<isOptOut()) { LOG(" but is opt-out!"); isOptOut = true; } LOG(endl); break; } LOG("Did not cover us ("< getZoneCuts(const DNSName& begin, const DNSName& end, DNSRecordOracle& dro) { vector ret; if(!begin.isPartOf(end)) throw PDNSException(end.toLogString() + "is not part of " + begin.toLogString()); DNSName qname(end); vector labelsToAdd = begin.makeRelative(end).getRawLabels(); // The shortest name is assumed to a zone cut ret.push_back(qname); while(qname != begin) { bool foundCut = false; if (labelsToAdd.empty()) break; qname.prependRawLabel(labelsToAdd.back()); labelsToAdd.pop_back(); auto records = dro.get(qname, (uint16_t)QType::NS); for (const auto& record : records) { if(record.d_type != QType::NS || record.d_name != qname) continue; foundCut = true; break; } if (foundCut) ret.push_back(qname); } return ret; } bool isRRSIGNotExpired(const time_t now, const shared_ptr& sig) { // Should use https://www.rfc-editor.org/rfc/rfc4034.txt section 3.1.5 return sig->d_sigexpire >= now; } bool isRRSIGIncepted(const time_t now, const shared_ptr& sig) { // Should use https://www.rfc-editor.org/rfc/rfc4034.txt section 3.1.5 return sig->d_siginception - g_signatureInceptionSkew <= now; } static bool checkSignatureWithKey(time_t now, const shared_ptr sig, const shared_ptr key, const std::string& msg) { bool result = false; try { /* rfc4035: - The validator's notion of the current time MUST be less than or equal to the time listed in the RRSIG RR's Expiration field. - The validator's notion of the current time MUST be greater than or equal to the time listed in the RRSIG RR's Inception field. */ if (isRRSIGIncepted(now, sig) && isRRSIGNotExpired(now, sig)) { auto dke = DNSCryptoKeyEngine::makeFromPublicKeyString(key->d_algorithm, key->d_key); result = dke->verify(msg, sig->d_signature); LOG("signature by key with tag "<d_tag<<" and algorithm "<d_algorithm)<<" was " << (result ? "" : "NOT ")<<"valid"<d_siginception - g_signatureInceptionSkew > now) ? "not yet valid" : "expired")<<" (inception: "<d_siginception<<", inception skew: "<d_sigexpire<<", now: "< >& signatures, const skeyset_t& keys, bool validateAllSigs) { bool foundKey = false; bool isValid = false; bool allExpired = true; bool noneIncepted = true; for(const auto& signature : signatures) { unsigned int labelCount = name.countLabels(); if (signature->d_labels > labelCount) { LOG(name<<": Discarding invalid RRSIG whose label count is "<d_labels<<" while the RRset owner name has only "<d_tag, signature->d_algorithm); if (keysMatchingTag.empty()) { LOG("No key provided for "<d_tag<<" and algorithm "<d_algorithm)<d_type)<first.first<<"/"<<)<getTag()<<" -> "<getZoneRepresentation()<first.first)<<"/"<first.second)<<" with "<second.signatures.size()<<" sigs"<first.first, i->second.records, i->second.signatures, keys, true) == vState::Secure) { validated[i->first] = i->second; } } } // returns vState // should return vState, zone cut and validated keyset // i.e. www.7bits.nl -> insecure/7bits.nl/[] // www.powerdnssec.org -> secure/powerdnssec.org/[keys] // www.dnssec-failed.org -> bogus/dnssec-failed.org/[] cspmap_t harvestCSPFromRecs(const vector& recs) { cspmap_t cspmap; for(const auto& rec : recs) { // cerr<<"res "<(rec); if (rrc) { cspmap[{rec.d_name,rrc->d_type}].signatures.push_back(rrc); } } else { cspmap[{rec.d_name, rec.d_type}].records.insert(rec.d_content); } } return cspmap; } bool getTrustAnchor(const map& anchors, const DNSName& zone, dsmap_t &res) { const auto& it = anchors.find(zone); if (it == anchors.cend()) { return false; } res = it->second; return true; } bool haveNegativeTrustAnchor(const map& negAnchors, const DNSName& zone, std::string& reason) { const auto& it = negAnchors.find(zone); if (it == negAnchors.cend()) { return false; } reason = it->second; return true; } vState validateDNSKeysAgainstDS(time_t now, const DNSName& zone, const dsmap_t& dsmap, const skeyset_t& tkeys, const sortedRecords_t& toSign, const vector >& sigs, skeyset_t& validkeys) { /* * Check all DNSKEY records against all DS records and place all DNSKEY records * that have DS records (that we support the algo for) in the tentative key storage */ for (const auto& dsrc : dsmap) { auto r = getByTag(tkeys, dsrc.d_tag, dsrc.d_algorithm); // cerr<<"looking at DS with tag "<d_tag<<" matching "<d_tag).size()<<" keys of which "<d_tag).size()<<" valid"<d_tag, sig->d_algorithm); if (bytag.empty()) { continue; } string msg = getMessageForRRSET(zone, *sig, toSign); for (const auto& key : bytag) { // cerr<<"validating : "; bool signIsValid = checkSignatureWithKey(now, sig, key, msg); if (signIsValid) { LOG("validation succeeded - whole DNSKEY set is valid"<d_protocol != 3) { continue; } validProtocol = true; } if (!zoneKey) { return vState::BogusNoZoneKeyBitSet; } if (!notRevoked) { return vState::BogusRevokedDNSKEY; } if (!validProtocol) { return vState::BogusInvalidDNSKEYProtocol; } return vState::BogusNoValidDNSKEY; } return vState::Secure; } vState getKeysFor(DNSRecordOracle& dro, const DNSName& zone, skeyset_t& keyset) { auto luaLocal = g_luaconfs.getLocal(); const auto anchors = luaLocal->dsAnchors; if (anchors.empty()) // Nothing to do here return vState::Insecure; // Determine the lowest (i.e. with the most labels) Trust Anchor for zone DNSName lowestTA("."); for (auto const &anchor : anchors) if (zone.isPartOf(anchor.first) && lowestTA.countLabels() < anchor.first.countLabels()) lowestTA = anchor.first; // Before searching for the keys, see if we have a Negative Trust Anchor. If // so, test if the NTA is valid and return an NTA state const auto negAnchors = luaLocal->negAnchors; if (!negAnchors.empty()) { DNSName lowestNTA; for (auto const &negAnchor : negAnchors) if (zone.isPartOf(negAnchor.first) && lowestNTA.countLabels() <= negAnchor.first.countLabels()) lowestNTA = negAnchor.first; if(!lowestNTA.empty()) { LOG("Found a Negative Trust Anchor for "< "< > sigs; sortedRecords_t toSign; skeyset_t tkeys; // tentative keys validkeys.clear(); // cerr<<"got DS for ["< (rec); if(rrc) { LOG("Got signature: "<getZoneRepresentation()<<" with tag "<d_tag<<", for type "<d_type)<d_type != QType::DNSKEY) continue; sigs.push_back(rrc); } } else if(rec.d_type == QType::DNSKEY) { auto drc=getRR (rec); if(drc) { tkeys.insert(drc); LOG("Inserting key with tag "<getTag()<<" and algorithm "<d_algorithm)<<": "<getZoneRepresentation()<second.records.cbegin(); j!=cspiter->second.records.cend(); j++) { const auto dsrc=std::dynamic_pointer_cast(*j); if(dsrc) { dsmap.insert(*dsrc); } } } } // There were no zone cuts (aka, we should never get here) return vState::BogusUnableToGetDNSKEYs; } bool isSupportedDS(const DSRecordContent& ds) { if (!DNSCryptoKeyEngine::isAlgorithmSupported(ds.d_algorithm)) { LOG("Discarding DS "< >& signatures) { for (const auto& sig : signatures) { if (sig) { return sig->d_signer; } } return DNSName(); } const std::string& vStateToString(vState state) { static const std::vector vStates = {"Indeterminate", "Insecure", "Secure", "NTA", "TA", "Bogus - No valid DNSKEY", "Bogus - Invalid denial", "Bogus - Unable to get DSs", "Bogus - Unable to get DNSKEYs", "Bogus - Self Signed DS", "Bogus - No RRSIG", "Bogus - No valid RRSIG", "Bogus - Missing negative indication", "Bogus - Signature not yet valid", "Bogus - Signature expired", "Bogus - Unsupported DNSKEY algorithm", "Bogus - Unsupported DS digest type", "Bogus - No zone key bit set", "Bogus - Revoked DNSKEY", "Bogus - Invalid DNSKEY Protocol" }; return vStates.at(static_cast(state)); } std::ostream& operator<<(std::ostream &os, const vState d) { os< dStates = {"no denial", "inconclusive", "nxdomain", "nxqtype", "empty non-terminal", "insecure", "opt-out"}; os<(d)); return os; } void updateDNSSECValidationState(vState& state, const vState stateUpdate) { if (stateUpdate == vState::TA) { state = vState::Secure; } else if (stateUpdate == vState::NTA) { state = vState::Insecure; } else if (vStateIsBogus(stateUpdate)) { state = stateUpdate; } else if (state == vState::Indeterminate) { state = stateUpdate; } else if (stateUpdate == vState::Insecure) { if (!vStateIsBogus(state)) { state = vState::Insecure; } } }