From: Evan Hunt Date: Fri, 22 May 2026 02:34:00 +0000 (-0700) Subject: Ensure NSEC authority does not cross zonecut boundary X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=d16413c8bbf94423c3483f812532027156b2235d;p=thirdparty%2Fbind9.git Ensure NSEC authority does not cross zonecut boundary When using a cached NSEC record to prove that a delegation is insecure, we now check that the signer name in the corresponding RRSIG is not above a known secure delegation point. This prevents a signed namespace from being downgraded to insecure using an NSEC record from the grandparent zone. Fixes: isc-projects/bind9#5967 Co-Authored-By: Ondřej Surý --- diff --git a/lib/dns/validator.c b/lib/dns/validator.c index d7136309edc..e7c4c28b0ab 100644 --- a/lib/dns/validator.c +++ b/lib/dns/validator.c @@ -171,6 +171,9 @@ static isc_result_t create_ds_fetch(dns_validator_t *val, dns_name_t *name, isc_job_cb callback, const char *caller); +static isc_result_t +view_find(dns_validator_t *val, dns_name_t *name, dns_rdatatype_t type); + /*% * Ensure the validator's rdatasets are marked as expired. */ @@ -248,6 +251,32 @@ validator_done(dns_validator_t *val, isc_result_t result) { isc_async_run(val->loop, val->cb, val); } +static bool +closer_secure_ds_exists(dns_validator_t *val, const dns_name_t *signer, + const dns_name_t *name) { + dns_fixedname_t fl; + dns_name_t *l = dns_fixedname_initname(&fl); + unsigned int n = dns_name_countlabels(name); + unsigned int s = dns_name_countlabels(signer); + + for (unsigned int i = s + 1; i < n; i++) { + isc_result_t result; + bool secure; + + dns_name_getlabelsequence(name, n - i, i, l); + result = view_find(val, l, dns_rdatatype_ds); + secure = (result == ISC_R_SUCCESS && + val->frdataset.trust >= dns_trust_secure); + disassociate_rdatasets(val); + + if (secure) { + return true; + } + } + + return false; +} + /*% * The is_insecure_referral() function is called as part of seeking the DS * record. Look in the NSEC or NSEC3 record returned from a DS query to see if @@ -258,6 +287,11 @@ validator_done(dns_validator_t *val, isc_result_t result) { * are going to treat the message as insecure and just assume the DS was at * the delegation. * + * If 'crossed' is not NULL, it is set to true when a referral is rejected + * because the NSEC/NSEC3 signer sits above a known secure delegation point. + * Such a proof is forged: the caller can stop the insecurity walk rather than + * descend into more attacker-supplied labels. + * * Returns: *\li #true the NS bitmap was set in the NSEC or NSEC3 record, or * the NSEC3 covers the name (in case of opt-out), or @@ -268,27 +302,31 @@ validator_done(dns_validator_t *val, isc_result_t result) { static bool is_insecure_referral(dns_validator_t *val, dns_name_t *name, dns_rdataset_t *rdataset, isc_result_t dbresult, - const char *caller) { + const char *caller, bool *crossed) { dns_fixedname_t fixed; dns_label_t hashlabel; - dns_name_t nsec3name; + dns_name_t nsec3name = DNS_NAME_INITEMPTY; dns_rdata_nsec3_t nsec3; - dns_rdataset_t set; + dns_rdataset_t set = DNS_RDATASET_INIT; int order; int scope; - bool found; + bool found = false; isc_buffer_t buffer; isc_result_t result; unsigned char hash[NSEC3_MAX_HASH_LENGTH]; unsigned char owner[NSEC3_MAX_HASH_LENGTH]; unsigned int length; + dns_fixedname_t fsigner; + dns_name_t *signer = NULL; + dns_rdataset_t sigset = DNS_RDATASET_INIT; + dns_rdata_t srdata = DNS_RDATA_INIT; + dns_rdata_rrsig_t sig; - REQUIRE(dbresult == DNS_R_NXRRSET || dbresult == DNS_R_NCACHENXRRSET); - - dns_rdataset_init(&set); - if (dbresult == DNS_R_NXRRSET) { + switch (dbresult) { + case DNS_R_NXRRSET: dns_rdataset_clone(rdataset, &set); - } else { + break; + case DNS_R_NCACHENXRRSET: result = dns_ncache_getrdataset(rdataset, name, dns_rdatatype_nsec, &set); if (result == ISC_R_NOTFOUND) { @@ -301,11 +339,16 @@ is_insecure_referral(dns_validator_t *val, dns_name_t *name, dns_rdataset_cleanup(&set); goto trynsec3; } + break; + default: + UNREACHABLE(); } INSIST(set.type == dns_rdatatype_nsec); - found = false; + /* + * Is there an NS in the NSEC? + */ result = dns_rdataset_first(&set); if (result == ISC_R_SUCCESS) { dns_rdata_t rdata = DNS_RDATA_INIT; @@ -313,33 +356,94 @@ is_insecure_referral(dns_validator_t *val, dns_name_t *name, found = dns_nsec_typepresent(&rdata, dns_rdatatype_ns); } dns_rdataset_disassociate(&set); + + /* + * Recover the NSEC's RRSIG signer so its authority can be bounded. A + * cached proof keeps the signature in the ncache blob; a live NSEC has + * it in the parallel signature rdataset. + */ + if (found) { + dns_rdataset_t *sigp = NULL; + + if (dbresult == DNS_R_NCACHENXRRSET) { + if (dns_ncache_getsigrdataset(rdataset, name, + dns_rdatatype_nsec, + &sigset) == ISC_R_SUCCESS) + { + sigp = &sigset; + } + } else if (dns_rdataset_isassociated(&val->fsigrdataset) && + val->fsigrdataset.covers == dns_rdatatype_nsec) + { + sigp = &val->fsigrdataset; + } + + if (sigp != NULL && dns_rdataset_first(sigp) == ISC_R_SUCCESS) { + dns_rdataset_current(sigp, &srdata); + if (dns_rdata_tostruct(&srdata, &sig, NULL) == + ISC_R_SUCCESS) + { + signer = dns_fixedname_initname(&fsigner); + dns_name_copy(&sig.signer, signer); + } + } + if (sigp == &sigset) { + dns_rdataset_cleanup(&sigset); + } + } + + if (signer != NULL && closer_secure_ds_exists(val, signer, name)) { + validator_log(val, ISC_LOG_DEBUG(3), + "is_insecure_referral: NSEC signer above known " + "secure DS; refusing insecure-delegation proof"); + found = false; + SET_IF_NOT_NULL(crossed, true); + } + return found; trynsec3: /* * Iterate over the ncache entry. */ - found = false; - dns_name_init(&nsec3name); dns_fixedname_init(&fixed); dns_name_downcase(name, dns_fixedname_name(&fixed)); name = dns_fixedname_name(&fixed); + DNS_RDATASET_FOREACH(rdataset) { + dns_rdataset_cleanup(&set); dns_ncache_current(rdataset, &nsec3name, &set); if (set.type != dns_rdatatype_nsec3) { - dns_rdataset_disassociate(&set); continue; } if (set.trust < dns_trust_secure) { dns_rdataset_cleanup(&set); continue; } + + /* + * Remember this NSEC3's zone (the owner name's parent) as the + * signer to bound. It is refreshed for every record so that, + * when one below triggers the terminal condition, 'signer' + * reflects that record -- not some earlier non-matching NSEC3. + * The bound check walks the cache and would disassociate + * 'rdataset' (== val->frdataset), so it runs only after the + * loop, at checksigner. + */ + unsigned int labels = dns_name_countlabels(&nsec3name); + if (labels > 1) { + dns_name_t parent = DNS_NAME_INITEMPTY; + dns_name_getlabelsequence(&nsec3name, 1, labels - 1, + &parent); + signer = dns_fixedname_initname(&fsigner); + dns_name_copy(&parent, signer); + } + dns_name_getlabel(&nsec3name, 0, &hashlabel); isc_region_consume(&hashlabel, 1); isc_buffer_init(&buffer, owner, sizeof(owner)); result = isc_base32hexnp_decoderegion(&hashlabel, &buffer); if (result != ISC_R_SUCCESS) { - dns_rdataset_disassociate(&set); continue; } DNS_RDATASET_FOREACH(&set) { @@ -361,8 +465,9 @@ trynsec3: validator_log(val, ISC_LOG_DEBUG(3), "%s: too many iterations", caller); + found = true; dns_rdataset_disassociate(&set); - return true; + goto checksigner; } length = isc_iterated_hash( hash, nsec3.hash, nsec3.iterations, @@ -376,7 +481,7 @@ trynsec3: found = dns_nsec3_typepresent(&rdata, dns_rdatatype_ns); dns_rdataset_disassociate(&set); - return found; + goto checksigner; } if ((nsec3.flags & DNS_NSEC3FLAG_OPTOUT) == 0) { continue; @@ -392,12 +497,32 @@ trynsec3: (order > 0 || memcmp(hash, nsec3.next.base, length) < 0))) { + found = true; dns_rdataset_disassociate(&set); - return true; + goto checksigner; } } - dns_rdataset_disassociate(&set); } + + dns_rdataset_cleanup(&set); + return found; + +checksigner: + /* + * The proof claims an insecure delegation. Reject it if the NSEC3 + * signer sits above a known secure delegation point: such a proof is + * forged by a zone above the real zone cut. + */ + if (found && signer != NULL && + closer_secure_ds_exists(val, signer, name)) + { + validator_log(val, ISC_LOG_DEBUG(3), + "is_insecure_referral: NSEC3 signer above known " + "secure DS; refusing insecure-delegation proof"); + SET_IF_NOT_NULL(crossed, true); + found = false; + } + return found; } @@ -633,10 +758,11 @@ fetch_callback_ds(void *arg) { result = proveunsecure(val, true, false, true); break; case DNS_R_NXRRSET: - case DNS_R_NCACHENXRRSET: + case DNS_R_NCACHENXRRSET: { + bool crossed = false; if (is_insecure_referral(val, resp->foundname, &val->frdataset, eresult, - "fetch_callback_ds")) + "fetch_callback_ds", &crossed)) { /* * Failed to find a DS while trying to prove @@ -646,7 +772,18 @@ fetch_callback_ds(void *arg) { result = markanswer(val, "fetch_callback_ds"); break; } - FALLTHROUGH; + if (crossed) { + /* + * The NSEC/NSEC3 signer sits above a known + * secure delegation, so this proof is forged. + * Stop instead of descending further. + */ + result = DNS_R_NOTINSECURE; + break; + } + result = proveunsecure(val, false, false, true); + break; + } case DNS_R_CNAME: /* * Not a zone cut, so we have to keep looking for @@ -751,16 +888,33 @@ validator_callback_ds(void *arg) { have_dsset = (val->frdataset.type == dns_rdatatype_ds); name = dns_fixedname_name(&val->fname); - if ((val->attributes & VALATTR_INSECURITY) != 0 && - val->frdataset.covers == dns_rdatatype_ds && - NEGATIVE(&val->frdataset) && - is_insecure_referral(val, name, &val->frdataset, - DNS_R_NCACHENXRRSET, - "validator_callback_ds")) - { - result = markanswer(val, "validator_callback_ds"); - } else if ((val->attributes & VALATTR_INSECURITY) != 0) { - result = proveunsecure(val, have_dsset, false, true); + if ((val->attributes & VALATTR_INSECURITY) != 0) { + bool crossed = false; + bool insecure = false; + + if (val->frdataset.covers == dns_rdatatype_ds && + NEGATIVE(&val->frdataset)) + { + insecure = is_insecure_referral( + val, name, &val->frdataset, + DNS_R_NCACHENXRRSET, + "validator_callback_ds", &crossed); + } + + if (insecure) { + result = markanswer(val, + "validator_callback_ds"); + } else if (crossed) { + /* + * The NSEC/NSEC3 signer sits above a known + * secure delegation, so this proof is forged. + * Stop instead of descending further. + */ + result = DNS_R_NOTINSECURE; + } else { + result = proveunsecure(val, have_dsset, false, + true); + } } else { result = validate_async_run(val, validate_dnskey); } @@ -3424,11 +3578,24 @@ seek_ds(dns_validator_t *val, isc_result_t *resp) { return ISC_R_COMPLETE; } - if (is_insecure_referral(val, tname, &val->frdataset, result, - "seek_ds")) { - *resp = markanswer(val, "seek_ds (3)"); - return ISC_R_COMPLETE; + bool crossed = false; + if (is_insecure_referral(val, tname, &val->frdataset, + result, "seek_ds", &crossed)) + { + *resp = markanswer(val, "seek_ds (3)"); + return ISC_R_COMPLETE; + } + if (crossed) { + /* + * The NSEC/NSEC3 signer sits above a known + * secure delegation, so this insecurity proof + * is forged. Stop walking instead of descending + * into more attacker-supplied labels. + */ + *resp = DNS_R_NOTINSECURE; + return ISC_R_COMPLETE; + } } break;