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.
*/
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
* 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
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) {
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;
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) {
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,
found = dns_nsec3_typepresent(&rdata,
dns_rdatatype_ns);
dns_rdataset_disassociate(&set);
- return found;
+ goto checksigner;
}
if ((nsec3.flags & DNS_NSEC3FLAG_OPTOUT) == 0) {
continue;
(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;
}
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
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
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);
}
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;