1
0
Fork 0
knot-resolver/lib/layer/validate.c
Daniel Baumann fbc604e215
Adding upstream version 5.7.5.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-21 13:56:17 +02:00

1400 lines
46 KiB
C

/* Copyright (C) CZ.NIC, z.s.p.o. <knot-resolver@labs.nic.cz>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#include <errno.h>
#include <sys/time.h>
#include <stdio.h>
#include <string.h>
#include <contrib/cleanup.h>
#include <libknot/packet/wire.h>
#include <libknot/rrtype/rdname.h>
#include <libknot/rrtype/rrsig.h>
#include <libdnssec/error.h>
#include "lib/dnssec/nsec.h"
#include "lib/dnssec/nsec3.h"
#include "lib/dnssec/ta.h"
#include "lib/dnssec.h"
#include "lib/layer.h"
#include "lib/resolve.h"
#include "lib/rplan.h"
#include "lib/utils.h"
#include "lib/defines.h"
#include "lib/module.h"
#include "lib/selection.h"
#define VERBOSE_MSG(qry, ...) kr_log_q(qry, VALIDATOR, __VA_ARGS__)
#define MAX_REVALIDATION_CNT 2
/**
* Search in section for given type.
* @param sec Packet section.
* @param type Type to search for.
* @return True if found.
*/
static bool section_has_type(const knot_pktsection_t *sec, uint16_t type)
{
if (!sec) {
return false;
}
for (unsigned i = 0; i < sec->count; ++i) {
const knot_rrset_t *rr = knot_pkt_rr(sec, i);
if (rr->type == type) {
return true;
}
}
return false;
}
static bool pkt_has_type(const knot_pkt_t *pkt, uint16_t type)
{
if (!pkt) {
return false;
}
if (section_has_type(knot_pkt_section(pkt, KNOT_ANSWER), type)) {
return true;
}
if (section_has_type(knot_pkt_section(pkt, KNOT_AUTHORITY), type)) {
return true;
}
return section_has_type(knot_pkt_section(pkt, KNOT_ADDITIONAL), type);
}
static void log_bogus_rrsig(kr_rrset_validation_ctx_t *vctx,
const knot_rrset_t *rr, const char *msg) {
if (kr_log_is_debug_qry(VALIDATOR, vctx->log_qry)) {
auto_free char *name_text = kr_dname_text(rr->owner);
auto_free char *type_text = kr_rrtype_text(rr->type);
VERBOSE_MSG(vctx->log_qry, ">< %s: %s %s "
"(%u matching RRSIGs, %u expired, %u not yet valid, "
"%u invalid signer, %u invalid label count, %u invalid key, "
"%u invalid crypto, %u invalid NSEC)\n",
msg, name_text, type_text, vctx->rrs_counters.matching_name_type,
vctx->rrs_counters.expired, vctx->rrs_counters.notyet,
vctx->rrs_counters.signer_invalid, vctx->rrs_counters.labels_invalid,
vctx->rrs_counters.key_invalid, vctx->rrs_counters.crypto_invalid,
vctx->rrs_counters.nsec_invalid);
}
}
/** Check that given CNAME could be generated by given DNAME (no DNSSEC validation). */
static bool cname_matches_dname(const knot_rrset_t *rr_cn, const knot_rrset_t *rr_dn)
{
if (kr_fails_assert(rr_cn->type == KNOT_RRTYPE_CNAME && rr_dn->type == KNOT_RRTYPE_DNAME))
return false;
/* When DNAME substitution happens, let's consider the "prefix"
* that is carried over and the "suffix" that is replaced.
* (Here we consider the label order used in wire and presentation.) */
const int prefix_labels = knot_dname_in_bailiwick(rr_cn->owner, rr_dn->owner);
if (prefix_labels < 1)
return false;
const knot_dname_t *cn_target = knot_cname_name(rr_cn->rrs.rdata);
const knot_dname_t *dn_target = knot_dname_target(rr_dn->rrs.rdata);
/* ^ We silently use the first RR in each RRset. Could be e.g. logged. */
/* Check that the suffixes are correct - and even prefix label counts. */
if (knot_dname_in_bailiwick(cn_target, dn_target) != prefix_labels)
return false;
/* Check that prefixes match. Find end of the first one and compare. */
const knot_dname_t *cn_se = rr_cn->owner;
for (int i = 0; i < prefix_labels; ++i)
cn_se += 1 + *cn_se;
return strncmp((const char *)rr_cn->owner, (const char *)cn_target,
cn_se - rr_cn->owner) == 0;
/* ^ We use the fact that dnames are always zero-terminated
* to avoid any possible over-read in cn_target. */
}
static void mark_insecure_parents(const struct kr_query *qry);
static void rank_records(struct kr_query *qry, bool any_rank, enum kr_rank rank_to_set,
const knot_dname_t *bailiwick);
static bool maybe_downgrade_nsec3(const ranked_rr_array_entry_t *e, struct kr_query *qry,
const kr_rrset_validation_ctx_t *vctx)
{
bool required_conditions =
e->rr->type == KNOT_RRTYPE_NSEC3
&& kr_rank_test(e->rank, KR_RANK_SECURE)
// extra careful: avoid downgrade if SNAME isn't in bailiwick of signer
&& knot_dname_in_bailiwick(qry->sname, vctx->zone_name) >= 0;
if (!required_conditions)
return false;
const knot_rdataset_t *rrs = &e->rr->rrs;
knot_rdata_t *rd = rrs->rdata;
for (int j = 0; j < rrs->count; ++j, rd = knot_rdataset_next(rd)) {
if (kr_nsec3_limited_rdata(rd))
goto do_downgrade;
}
return false;
do_downgrade: // we do this deep inside calls because of having signer name available
VERBOSE_MSG(qry,
"<= DNSSEC downgraded due to expensive NSEC3: %d iterations, %d salt length\n",
(int)knot_nsec3_iters(rd), (int)knot_nsec3_salt_len(rd));
qry->flags.DNSSEC_WANT = false;
qry->flags.DNSSEC_INSECURE = true;
rank_records(qry, true, KR_RANK_INSECURE, vctx->zone_name);
mark_insecure_parents(qry);
return true;
}
static int validate_section(kr_rrset_validation_ctx_t *vctx, struct kr_query *qry,
knot_mm_t *pool)
{
struct kr_request *req = qry->request;
if (!vctx) {
return kr_error(EINVAL);
}
/* Can't use qry->zone_cut.name directly, as this name can
* change when updating cut information before validation.
*/
vctx->zone_name = vctx->keys ? vctx->keys->owner : NULL;
for (ssize_t i = 0; i < vctx->rrs->len; ++i) {
ranked_rr_array_entry_t *entry = vctx->rrs->at[i];
knot_rrset_t * const rr = entry->rr;
if (entry->yielded || vctx->qry_uid != entry->qry_uid) {
continue;
}
if (kr_rank_test(entry->rank, KR_RANK_OMIT)
|| kr_rank_test(entry->rank, KR_RANK_SECURE)) {
continue; /* these are already OK */
}
if (!knot_dname_is_equal(qry->zone_cut.name, rr->owner)/*optim.*/
&& !kr_ta_closest(qry->request->ctx, rr->owner, rr->type)) {
/* We have NTA "between" our (perceived) zone cut and the RR. */
kr_rank_set(&entry->rank, KR_RANK_INSECURE);
continue;
}
if (rr->type == KNOT_RRTYPE_RRSIG) {
const knot_dname_t *signer_name = knot_rrsig_signer_name(rr->rrs.rdata);
if (!knot_dname_is_equal(vctx->zone_name, signer_name)) {
kr_rank_set(&entry->rank, KR_RANK_MISMATCH);
vctx->err_cnt += 1;
break;
}
if (!kr_rank_test(entry->rank, KR_RANK_BOGUS))
kr_rank_set(&entry->rank, KR_RANK_OMIT);
continue;
}
uint8_t rank_orig = entry->rank;
int validation_result = kr_rrset_validate(vctx, rr);
/* Handle the case of CNAMEs synthesized from DNAMEs (they don't have RRSIGs). */
if (rr->type == KNOT_RRTYPE_CNAME && validation_result == kr_error(ENOENT)) {
for (ssize_t j = 0; j < vctx->rrs->len; ++j) {
ranked_rr_array_entry_t *e_dname = vctx->rrs->at[j];
if ((e_dname->rr->type == KNOT_RRTYPE_DNAME)
/* If the order is wrong, we will need two passes. */
&& kr_rank_test(e_dname->rank, KR_RANK_SECURE)
&& cname_matches_dname(rr, e_dname->rr)) {
/* Now we believe the CNAME is OK. */
validation_result = kr_ok();
break;
}
}
if (validation_result != kr_ok()) {
vctx->cname_norrsig_cnt += 1;
}
}
if (validation_result == kr_ok()) {
kr_rank_set(&entry->rank, KR_RANK_SECURE);
/* Downgrade zone to insecure if certain NSEC3 record occurs. */
if (unlikely(maybe_downgrade_nsec3(entry, qry, vctx)))
return kr_error(KNOT_EDOWNGRADED);
} else if (kr_rank_test(rank_orig, KR_RANK_TRY)) {
/* RFC 4035 section 2.2:
* NS RRsets that appear at delegation points (...)
* MUST NOT be signed */
if (vctx->rrs_counters.matching_name_type > 0)
log_bogus_rrsig(vctx, rr,
"found unexpected signatures for non-authoritative data which failed to validate, continuing");
vctx->result = kr_ok();
kr_rank_set(&entry->rank, KR_RANK_TRY);
/* ^^ BOGUS would be more accurate, but it might change
* to MISMATCH on revalidation, e.g. in test val_referral_nods :-/
*/
} else if (validation_result == kr_error(ENOENT)
&& vctx->rrs_counters.matching_name_type == 0) {
/* no RRSIGs found */
kr_rank_set(&entry->rank, KR_RANK_MISSING);
vctx->err_cnt += 1;
kr_request_set_extended_error(req, KNOT_EDNS_EDE_RRSIG_MISS, "JZAJ");
log_bogus_rrsig(vctx, rr, "no valid RRSIGs found");
} else {
kr_rank_set(&entry->rank, KR_RANK_BOGUS);
vctx->err_cnt += 1;
if (vctx->rrs_counters.expired > 0)
kr_request_set_extended_error(req, KNOT_EDNS_EDE_SIG_EXPIRED, "YFJ2");
else if (vctx->rrs_counters.notyet > 0)
kr_request_set_extended_error(req, KNOT_EDNS_EDE_SIG_NOTYET, "UBBS");
else
kr_request_set_extended_error(req, KNOT_EDNS_EDE_BOGUS, "I74V");
log_bogus_rrsig(vctx, rr, "bogus signatures");
}
}
return kr_ok();
}
static int validate_records(struct kr_request *req, knot_pkt_t *answer, knot_mm_t *pool, bool has_nsec3)
{
struct kr_query *qry = req->current_query;
if (!qry->zone_cut.key) {
VERBOSE_MSG(qry, "<= no DNSKEY, can't validate\n");
return kr_error(EBADMSG);
}
kr_rrset_validation_ctx_t vctx = {
.pkt = answer,
.rrs = &req->answ_selected,
.section_id = KNOT_ANSWER,
.keys = qry->zone_cut.key,
.zone_name = qry->zone_cut.name,
.timestamp = qry->timestamp.tv_sec,
.ttl_min = req->ctx->cache.ttl_min,
.qry_uid = qry->uid,
.has_nsec3 = has_nsec3,
.flags = 0,
.err_cnt = 0,
.cname_norrsig_cnt = 0,
.result = 0,
.limit_crypto_remains = &qry->vld_limit_crypto_remains,
.log_qry = qry,
};
int ret = validate_section(&vctx, qry, pool);
if (vctx.err_cnt && vctx.err_cnt == vctx.cname_norrsig_cnt) {
VERBOSE_MSG(qry, ">< all validation errors are missing RRSIGs on CNAMES, trying again in hope for DNAMEs\n");
vctx.err_cnt = vctx.cname_norrsig_cnt = vctx.result = 0;
ret = validate_section(&vctx, qry, pool);
}
req->answ_validated = (vctx.err_cnt == 0);
if (ret != kr_ok()) {
return ret;
}
uint32_t an_flags = vctx.flags;
vctx.rrs = &req->auth_selected;
vctx.section_id = KNOT_AUTHORITY;
vctx.flags = 0;
vctx.err_cnt = 0;
vctx.result = 0;
ret = validate_section(&vctx, qry, pool);
req->auth_validated = (vctx.err_cnt == 0);
if (ret != kr_ok()) {
return ret;
}
/* Records were validated.
* If there is wildcard expansion in answer,
* or optout - flag the query.
*/
if (an_flags & KR_DNSSEC_VFLG_WEXPAND) {
qry->flags.DNSSEC_WEXPAND = true;
}
if (an_flags & KR_DNSSEC_VFLG_OPTOUT) {
qry->flags.DNSSEC_OPTOUT = true;
}
return ret;
}
static int validate_keyset(struct kr_request *req, knot_pkt_t *answer, bool has_nsec3)
{
/* Merge DNSKEY records from answer that are below/at current cut. */
struct kr_query *qry = req->current_query;
bool updated_key = false;
const knot_pktsection_t *an = knot_pkt_section(answer, KNOT_ANSWER);
for (unsigned i = 0; i < an->count; ++i) {
const knot_rrset_t *rr = knot_pkt_rr(an, i);
if (rr->type != KNOT_RRTYPE_DNSKEY
|| knot_dname_in_bailiwick(rr->owner, qry->zone_cut.name) < 0) {
continue;
}
/* Merge with zone cut (or replace ancestor key). */
if (!qry->zone_cut.key || !knot_dname_is_equal(qry->zone_cut.key->owner, rr->owner)) {
qry->zone_cut.key = knot_rrset_copy(rr, qry->zone_cut.pool);
if (!qry->zone_cut.key) {
return kr_error(ENOMEM);
}
updated_key = true;
} else {
int ret = knot_rdataset_merge(&qry->zone_cut.key->rrs,
&rr->rrs, qry->zone_cut.pool);
if (ret != 0) {
knot_rrset_free(qry->zone_cut.key, qry->zone_cut.pool);
qry->zone_cut.key = NULL;
return ret;
}
updated_key = true;
}
}
/* Check if there's a key for current TA. */
if (updated_key && !(qry->flags.CACHED)) {
/* Find signatures for the DNSKEY; selected by iterator from ANSWER. */
int sig_index = -1;
for (int i = req->answ_selected.len - 1; i >= 0; --i) {
const knot_rrset_t *rrsig = req->answ_selected.at[i]->rr;
const bool ok = req->answ_selected.at[i]->qry_uid == qry->uid
&& rrsig->type == KNOT_RRTYPE_RRSIG
&& knot_rrsig_type_covered(rrsig->rrs.rdata)
== KNOT_RRTYPE_DNSKEY
&& rrsig->rclass == KNOT_CLASS_IN
&& knot_dname_is_equal(rrsig->owner,
qry->zone_cut.key->owner);
if (ok) {
sig_index = i;
break;
}
}
if (sig_index < 0) {
kr_request_set_extended_error(req, KNOT_EDNS_EDE_RRSIG_MISS, "EZDC");
return kr_error(ENOENT);
}
const knot_rdataset_t *sig_rds = &req->answ_selected.at[sig_index]->rr->rrs;
kr_rrset_validation_ctx_t vctx = {
.pkt = answer,
.rrs = &req->answ_selected,
.section_id = KNOT_ANSWER,
.keys = qry->zone_cut.key,
.zone_name = qry->zone_cut.name,
.timestamp = qry->timestamp.tv_sec,
.ttl_min = req->ctx->cache.ttl_min,
.qry_uid = qry->uid,
.has_nsec3 = has_nsec3,
.flags = 0,
.result = 0,
.limit_crypto_remains = &qry->vld_limit_crypto_remains,
.log_qry = qry,
};
int ret = kr_dnskeys_trusted(&vctx, sig_rds, qry->zone_cut.trust_anchor);
/* Set rank of the RRSIG. This may be needed, but I don't know why.
* In particular, black_ent.rpl may get broken otherwise. */
kr_rank_set(&req->answ_selected.at[sig_index]->rank,
ret == 0 ? KR_RANK_SECURE : KR_RANK_BOGUS);
if (ret != 0) {
log_bogus_rrsig(&vctx, qry->zone_cut.key, "bogus key");
knot_rrset_free(qry->zone_cut.key, qry->zone_cut.pool);
qry->zone_cut.key = NULL;
if (vctx.rrs_counters.expired > 0)
kr_request_set_extended_error(req, KNOT_EDNS_EDE_SIG_EXPIRED, "6GJV");
else if (vctx.rrs_counters.notyet > 0)
kr_request_set_extended_error(req, KNOT_EDNS_EDE_SIG_NOTYET, "4DJQ");
else
kr_request_set_extended_error(req, KNOT_EDNS_EDE_BOGUS, "EXRU");
return ret;
}
if (vctx.flags & KR_DNSSEC_VFLG_WEXPAND) {
qry->flags.DNSSEC_WEXPAND = true;
}
if (vctx.flags & KR_DNSSEC_VFLG_OPTOUT) {
qry->flags.DNSSEC_OPTOUT = true;
}
}
return kr_ok();
}
static knot_rrset_t *update_ds(struct kr_zonecut *cut, const knot_pktsection_t *sec)
{
/* Aggregate DS records (if using multiple keys) */
knot_rrset_t *new_ds = NULL;
for (unsigned i = 0; i < sec->count; ++i) {
const knot_rrset_t *rr = knot_pkt_rr(sec, i);
if (rr->type != KNOT_RRTYPE_DS) {
continue;
}
int ret = 0;
if (new_ds) {
ret = knot_rdataset_merge(&new_ds->rrs, &rr->rrs, cut->pool);
} else {
new_ds = knot_rrset_copy(rr, cut->pool);
if (!new_ds) {
return NULL;
}
}
if (ret != 0) {
knot_rrset_free(new_ds, cut->pool);
return NULL;
}
}
return new_ds;
}
static void mark_insecure_parents(const struct kr_query *qry)
{
/* If there is a chain of DS queries mark all of them,
* then mark first non-DS parent.
* Stop if parent is waiting for ns address.
* NS can be located at unsigned zone, but still will return
* valid DNSSEC records for initial query. */
struct kr_query *parent = qry->parent;
while (parent && !parent->flags.AWAIT_IPV4 && !parent->flags.AWAIT_IPV6) {
parent->flags.DNSSEC_WANT = false;
parent->flags.DNSSEC_INSECURE = true;
if (parent->stype != KNOT_RRTYPE_DS &&
parent->stype != KNOT_RRTYPE_RRSIG) {
break;
}
parent = parent->parent;
}
}
static int update_parent_keys(struct kr_request *req, uint16_t answer_type)
{
struct kr_query *qry = req->current_query;
struct kr_query *parent = qry->parent;
if (kr_fails_assert(parent))
return KR_STATE_FAIL;
switch(answer_type) {
case KNOT_RRTYPE_DNSKEY:
VERBOSE_MSG(qry, "<= parent: updating DNSKEY\n");
parent->zone_cut.key = knot_rrset_copy(qry->zone_cut.key, parent->zone_cut.pool);
if (!parent->zone_cut.key) {
return KR_STATE_FAIL;
}
break;
case KNOT_RRTYPE_DS:
VERBOSE_MSG(qry, "<= parent: updating DS\n");
if (qry->flags.DNSSEC_INSECURE) { /* DS non-existence proven. */
mark_insecure_parents(qry);
} else if (qry->flags.DNSSEC_NODS && !qry->flags.FORWARD) {
if (qry->flags.DNSSEC_OPTOUT) {
mark_insecure_parents(qry);
} else {
int ret = kr_dnssec_matches_name_and_type(&req->auth_selected, qry->uid,
qry->sname, KNOT_RRTYPE_NS);
if (ret == kr_ok()) {
mark_insecure_parents(qry);
}
}
} else if (qry->flags.DNSSEC_NODS && qry->flags.FORWARD) {
int ret = kr_dnssec_matches_name_and_type(&req->auth_selected, qry->uid,
qry->sname, KNOT_RRTYPE_NS);
if (ret == kr_ok()) {
mark_insecure_parents(qry);
}
} else { /* DS existence proven. */
parent->zone_cut.trust_anchor = knot_rrset_copy(qry->zone_cut.trust_anchor, parent->zone_cut.pool);
if (!parent->zone_cut.trust_anchor) {
return KR_STATE_FAIL;
}
}
break;
default: break;
}
return kr_ok();
}
static int update_delegation(struct kr_request *req, struct kr_query *qry, knot_pkt_t *answer, bool has_nsec3)
{
struct kr_zonecut *cut = &qry->zone_cut;
/* RFC4035 3.1.4. authoritative must send either DS or proof of non-existence.
* If it contains neither, resolver must query the parent for the DS (RFC4035 5.2.).
* If DS exists, the referral is OK,
* otherwise referral is bogus (or an attempted downgrade attack).
*/
unsigned section = KNOT_ANSWER;
const bool referral = !knot_wire_get_aa(answer->wire);
if (referral) {
section = KNOT_AUTHORITY;
} else if (knot_pkt_qtype(answer) == KNOT_RRTYPE_DS &&
!(qry->flags.CNAME) &&
(knot_wire_get_rcode(answer->wire) != KNOT_RCODE_NXDOMAIN)) {
section = KNOT_ANSWER;
} else { /* N/A */
return kr_ok();
}
int ret = 0;
const knot_dname_t *proved_name = knot_pkt_qname(answer);
/* Aggregate DS records (if using multiple keys) */
knot_rrset_t *new_ds = update_ds(cut, knot_pkt_section(answer, section));
if (!new_ds) {
/* No DS provided, check for proof of non-existence. */
if (!has_nsec3) {
if (referral) {
/* Check if it is referral to unsigned, rfc4035 5.2 */
ret = kr_nsec_ref_to_unsigned(&req->auth_selected,
qry->uid, proved_name);
} else {
/* No-data answer */
ret = kr_nsec_negative(&req->auth_selected, qry->uid,
proved_name, KNOT_RRTYPE_DS);
if (ret >= 0) {
if (ret == PKT_NODATA) {
ret = kr_ok();
} else {
ret = kr_error(ENOENT); // suspicious
}
}
}
} else {
if (referral) {
/* Check if it is referral to unsigned, rfc5155 8.9 */
ret = kr_nsec3_ref_to_unsigned(answer);
} else {
/* No-data answer, QTYPE is DS, rfc5155 8.6 */
ret = kr_nsec3_no_data(answer, KNOT_AUTHORITY, proved_name, KNOT_RRTYPE_DS);
}
if (ret == kr_error(KNOT_ERANGE)) {
/* Not bogus, going insecure due to optout */
ret = 0;
}
}
if (referral && qry->stype != KNOT_RRTYPE_DS &&
ret == DNSSEC_NOT_FOUND) {
/* referral,
* qtype is not KNOT_RRTYPE_DS, NSEC\NSEC3 were not found.
* Check if DS already was fetched. */
knot_rrset_t *ta = cut->trust_anchor;
if (knot_dname_is_equal(cut->name, ta->owner)) {
/* DS is OK */
ret = 0;
}
} else if (ret != 0) {
VERBOSE_MSG(qry, "<= bogus proof of DS non-existence\n");
kr_request_set_extended_error(req, KNOT_EDNS_EDE_BOGUS, "Z4I6");
qry->flags.DNSSEC_BOGUS = true;
} else if (proved_name[0] != '\0') { /* don't go to insecure for . DS */
qry->flags.DNSSEC_NODS = true;
/* Rank the corresponding nonauth NS as insecure. */
for (int i = 0; i < req->auth_selected.len; ++i) {
ranked_rr_array_entry_t *ns = req->auth_selected.at[i];
if (ns->qry_uid != qry->uid
|| !ns->rr
|| ns->rr->type != KNOT_RRTYPE_NS) {
continue;
}
if (!referral && !knot_dname_is_equal(qry->sname, ns->rr->owner)) {
continue;
}
/* Found the record. Note: this is slightly fragile
* in case there were more NS records in the packet.
* As it is now for referrals, kr_nsec*_ref_to_unsigned consider
* (only) the first NS record in the packet. */
if (!kr_rank_test(ns->rank, KR_RANK_AUTH)) { /* sanity */
ns->rank = KR_RANK_INSECURE;
}
break;
}
}
return ret;
} else if (qry->flags.FORWARD && qry->parent) {
struct kr_query *parent = qry->parent;
parent->zone_cut.name = knot_dname_copy(qry->sname, parent->zone_cut.pool);
}
/* Extend trust anchor */
VERBOSE_MSG(qry, "<= DS: OK\n");
cut->trust_anchor = new_ds;
return ret;
}
static const knot_dname_t *find_first_signer(ranked_rr_array_t *arr, struct kr_query *qry)
{
for (size_t i = 0; i < arr->len; ++i) {
ranked_rr_array_entry_t *entry = arr->at[i];
const knot_rrset_t *rr = entry->rr;
if (entry->yielded ||
(!kr_rank_test(entry->rank, KR_RANK_INITIAL) &&
!kr_rank_test(entry->rank, KR_RANK_TRY) &&
!kr_rank_test(entry->rank, KR_RANK_MISMATCH))) {
continue;
}
if (rr->type != KNOT_RRTYPE_RRSIG) {
continue;
}
const knot_dname_t *signame = knot_rrsig_signer_name(rr->rrs.rdata);
if (knot_dname_in_bailiwick(rr->owner, signame) >= 0) {
return signame;
} else {
/* otherwise it's some nonsense, so we skip it */
kr_log_q(qry, VALIDATOR, "protocol violation: "
"out-of-bailiwick RRSIG signer, skipping\n");
}
}
return NULL;
}
static const knot_dname_t *signature_authority(struct kr_request *req)
{
const knot_dname_t *signer_name = find_first_signer(&req->answ_selected, req->current_query);
if (!signer_name) {
signer_name = find_first_signer(&req->auth_selected, req->current_query);
}
return signer_name;
}
static int rrsig_not_found(const kr_layer_t * const ctx, const knot_pkt_t * const pkt,
const knot_rrset_t * const rr)
{
/* Signatures are missing. There might be a zone cut that we've skipped
* and transitions to insecure. That can commonly happen when iterating
* and both sides of that cut are served by the same IP address(es).
* We'll try proving that the name truly is insecure - by spawning
* a DS sub-query on a suitable QNAME.
*/
struct kr_request * const req = ctx->req;
struct kr_query * const qry = req->current_query;
if (qry->flags.FORWARD || qry->flags.STUB) {
/* Undiscovered signed cuts can't happen in the current forwarding
* algorithm, so this function shouldn't be able to help. */
return KR_STATE_FAIL;
}
/* Find cut_next: the name at which to try finding the "missing" zone cut. */
const knot_dname_t * const cut_top = qry->zone_cut.name;
const int next_depth = knot_dname_in_bailiwick(rr->owner, cut_top);
if (next_depth <= 0) {
return KR_STATE_FAIL; // shouldn't happen, I think
}
/* Add one extra label to cur_top, i.e. descend one level below current zone cut */
const knot_dname_t * const cut_next = rr->owner +
kr_dname_prefixlen(rr->owner, next_depth - 1);
/* Spawn that DS sub-query. */
struct kr_query * const next = kr_rplan_push(&req->rplan, qry, cut_next,
rr->rclass, KNOT_RRTYPE_DS);
if (!next) {
return KR_STATE_FAIL;
}
kr_zonecut_init(&next->zone_cut, qry->zone_cut.name, &req->pool);
kr_zonecut_copy(&next->zone_cut, &qry->zone_cut);
kr_zonecut_copy_trust(&next->zone_cut, &qry->zone_cut);
next->flags.DNSSEC_WANT = true;
return KR_STATE_YIELD;
}
static int check_validation_result(kr_layer_t *ctx, const knot_pkt_t *pkt, ranked_rr_array_t *arr)
{
int ret = KR_STATE_DONE;
struct kr_request *req = ctx->req;
struct kr_query *qry = req->current_query;
ranked_rr_array_entry_t *invalid_entry = NULL;
for (size_t i = 0; i < arr->len; ++i) {
ranked_rr_array_entry_t *entry = arr->at[i];
if (entry->yielded || entry->qry_uid != qry->uid) {
continue;
}
if (kr_rank_test(entry->rank, KR_RANK_MISMATCH)) {
invalid_entry = entry;
break;
} else if (kr_rank_test(entry->rank, KR_RANK_MISSING) &&
!invalid_entry) { // NOLINT(bugprone-branch-clone)
invalid_entry = entry;
} else if (kr_rank_test(entry->rank, KR_RANK_OMIT)) {
continue;
} else if (!kr_rank_test(entry->rank, KR_RANK_SECURE) &&
!invalid_entry) {
invalid_entry = entry;
}
}
if (!invalid_entry) {
return ret;
}
if (!kr_rank_test(invalid_entry->rank, KR_RANK_SECURE) &&
(++(invalid_entry->revalidation_cnt) > MAX_REVALIDATION_CNT)) {
VERBOSE_MSG(qry, "<= continuous revalidation, fails\n");
kr_request_set_extended_error(req, KNOT_EDNS_EDE_OTHER,
"4T4L: continuous revalidation");
qry->flags.DNSSEC_BOGUS = true;
return KR_STATE_FAIL;
}
const knot_rrset_t *rr = invalid_entry->rr;
if (kr_rank_test(invalid_entry->rank, KR_RANK_MISMATCH)) {
const knot_dname_t *signer_name = knot_rrsig_signer_name(rr->rrs.rdata);
if (knot_dname_in_bailiwick(signer_name, qry->zone_cut.name) > 0) {
qry->zone_cut.name = knot_dname_copy(signer_name, &req->pool);
qry->flags.AWAIT_CUT = true;
} else if (!knot_dname_is_equal(signer_name, qry->zone_cut.name)) {
if (qry->zone_cut.parent) {
memcpy(&qry->zone_cut, qry->zone_cut.parent, sizeof(qry->zone_cut));
} else {
qry->flags.AWAIT_CUT = true;
}
qry->zone_cut.name = knot_dname_copy(signer_name, &req->pool);
}
VERBOSE_MSG(qry, ">< cut changed (new signer), needs revalidation\n");
ret = KR_STATE_YIELD;
} else if (kr_rank_test(invalid_entry->rank, KR_RANK_MISSING)) {
ret = rrsig_not_found(ctx, pkt, rr);
} else if (!kr_rank_test(invalid_entry->rank, KR_RANK_SECURE)) {
kr_request_set_extended_error(req, KNOT_EDNS_EDE_BOGUS, "NXJA");
qry->flags.DNSSEC_BOGUS = true;
ret = KR_STATE_FAIL;
}
return ret;
}
static bool check_empty_answer(kr_layer_t *ctx, knot_pkt_t *pkt)
{
struct kr_request *req = ctx->req;
struct kr_query *qry = req->current_query;
ranked_rr_array_t *arr = &req->answ_selected;
size_t num_entries = 0;
for (size_t i = 0; i < arr->len; ++i) {
ranked_rr_array_entry_t *entry = arr->at[i];
const knot_rrset_t *rr = entry->rr;
if (rr->type == KNOT_RRTYPE_RRSIG && qry->stype != KNOT_RRTYPE_RRSIG) {
continue;
}
if (entry->qry_uid == qry->uid) {
++num_entries;
}
}
const knot_pktsection_t *an = knot_pkt_section(pkt, KNOT_ANSWER);
return ((an->count != 0) && (num_entries == 0)) ? false : true;
}
static int unsigned_forward(kr_layer_t *ctx, knot_pkt_t *pkt)
{
struct kr_request *req = ctx->req;
struct kr_query *qry = req->current_query;
const uint16_t qtype = knot_pkt_qtype(pkt);
const uint8_t pkt_rcode = knot_wire_get_rcode(pkt->wire);
bool nods = false;
bool ns_exist = true;
for (int i = 0; i < req->rplan.resolved.len; ++i) {
struct kr_query *q = req->rplan.resolved.at[i];
if (q->sclass == qry->sclass &&
q->stype == KNOT_RRTYPE_DS &&
knot_dname_is_equal(q->sname, qry->sname)) {
nods = true;
if (!(q->flags.DNSSEC_OPTOUT)) {
int ret = kr_dnssec_matches_name_and_type(&req->auth_selected, q->uid,
qry->sname, KNOT_RRTYPE_NS);
ns_exist = (ret == kr_ok());
}
}
}
if (nods && ns_exist && qtype == KNOT_RRTYPE_NS) {
qry->flags.DNSSEC_WANT = false;
qry->flags.DNSSEC_INSECURE = true;
if (qry->forward_flags.CNAME) {
if (kr_fails_assert(qry->cname_parent))
return KR_STATE_FAIL;
qry->cname_parent->flags.DNSSEC_WANT = false;
qry->cname_parent->flags.DNSSEC_INSECURE = true;
} else if (pkt_rcode == KNOT_RCODE_NOERROR && qry->parent != NULL) {
const knot_pktsection_t *sec = knot_pkt_section(pkt, KNOT_ANSWER);
const knot_rrset_t *rr = knot_pkt_rr(sec, 0);
if (rr->type == KNOT_RRTYPE_NS) {
qry->parent->zone_cut.name = knot_dname_copy(rr->owner, &req->pool);
qry->parent->flags.DNSSEC_WANT = false;
qry->parent->flags.DNSSEC_INSECURE = true;
}
}
while (qry->parent) {
qry = qry->parent;
qry->flags.DNSSEC_WANT = false;
qry->flags.DNSSEC_INSECURE = true;
if (qry->forward_flags.CNAME) {
if (kr_fails_assert(qry->cname_parent))
return KR_STATE_FAIL;
qry->cname_parent->flags.DNSSEC_WANT = false;
qry->cname_parent->flags.DNSSEC_INSECURE = true;
}
}
return KR_STATE_DONE;
}
if (ctx->state == KR_STATE_YIELD) {
return KR_STATE_DONE;
}
if (!nods && qtype != KNOT_RRTYPE_DS) {
struct kr_rplan *rplan = &req->rplan;
struct kr_query *next = kr_rplan_push(rplan, qry, qry->sname, qry->sclass, KNOT_RRTYPE_DS);
if (!next) {
return KR_STATE_FAIL;
}
kr_zonecut_set(&next->zone_cut, qry->zone_cut.name);
kr_zonecut_copy_trust(&next->zone_cut, &qry->zone_cut);
next->flags.DNSSEC_WANT = true;
}
return KR_STATE_YIELD;
}
static int check_signer(kr_layer_t *ctx, knot_pkt_t *pkt)
{
struct kr_request *req = ctx->req;
struct kr_query *qry = req->current_query;
const knot_dname_t *ta_name = qry->zone_cut.trust_anchor ? qry->zone_cut.trust_anchor->owner : NULL;
const knot_dname_t *signer = signature_authority(req);
if (ta_name && (!signer || !knot_dname_is_equal(ta_name, signer))) {
/* check all newly added RRSIGs */
if (!signer) {
if (qry->flags.FORWARD) {
return unsigned_forward(ctx, pkt);
}
/* Not a DNSSEC-signed response. */
if (ctx->state == KR_STATE_YIELD) {
/* Already yielded for revalidation.
* It means that trust chain is OK and
* transition to INSECURE hasn't occurred.
* Let the validation logic ask about RRSIG. */
return KR_STATE_DONE;
}
/* Ask parent for DS
* to prove transition to INSECURE. */
const uint16_t qtype = knot_pkt_qtype(pkt);
const knot_dname_t *qname = knot_pkt_qname(pkt);
if (qtype == KNOT_RRTYPE_NS &&
knot_dname_in_bailiwick(qname, qry->zone_cut.name) > 0) {
/* Server is authoritative
* for both parent and child,
* and child zone is not signed. */
qry->zone_cut.name = knot_dname_copy(qname, &req->pool);
}
} else if (knot_dname_in_bailiwick(signer, qry->zone_cut.name) > 0) {
if (!(qry->flags.FORWARD)) {
/* Key signer is below current cut, advance and refetch keys. */
qry->zone_cut.name = knot_dname_copy(signer, &req->pool);
} else {
/* Check if DS does not exist. */
struct kr_query *q = kr_rplan_find_resolved(&req->rplan, NULL,
signer, qry->sclass, KNOT_RRTYPE_DS);
if (q && q->flags.DNSSEC_NODS) {
qry->flags.DNSSEC_WANT = false;
qry->flags.DNSSEC_INSECURE = true;
if (qry->parent) {
qry->parent->flags.DNSSEC_WANT = false;
qry->parent->flags.DNSSEC_INSECURE = true;
}
} else if (qry->stype != KNOT_RRTYPE_DS) {
struct kr_rplan *rplan = &req->rplan;
struct kr_query *next = kr_rplan_push(rplan, qry, qry->sname,
qry->sclass, KNOT_RRTYPE_DS);
if (!next) {
return KR_STATE_FAIL;
}
kr_zonecut_set(&next->zone_cut, qry->zone_cut.name);
kr_zonecut_copy_trust(&next->zone_cut, &qry->zone_cut);
next->flags.DNSSEC_WANT = true;
}
}
} else if (!knot_dname_is_equal(signer, qry->zone_cut.name)) {
/* Key signer is above the current cut, so we can't validate it. This happens when
a server is authoritative for both grandparent, parent and child zone.
Ascend to parent cut, and refetch authority for signer. */
if (qry->zone_cut.parent) {
memcpy(&qry->zone_cut, qry->zone_cut.parent, sizeof(qry->zone_cut));
} else {
qry->flags.AWAIT_CUT = true;
}
qry->zone_cut.name = knot_dname_copy(signer, &req->pool);
}
/* zone cut matches, but DS/DNSKEY doesn't => refetch. */
VERBOSE_MSG(qry, ">< cut changed, needs revalidation\n");
if ((qry->flags.FORWARD) && qry->stype != KNOT_RRTYPE_DS) {
struct kr_rplan *rplan = &req->rplan;
struct kr_query *next = kr_rplan_push(rplan, qry, signer,
qry->sclass, KNOT_RRTYPE_DS);
if (!next) {
return KR_STATE_FAIL;
}
kr_zonecut_set(&next->zone_cut, qry->zone_cut.name);
kr_zonecut_copy_trust(&next->zone_cut, &qry->zone_cut);
next->flags.DNSSEC_WANT = true;
return KR_STATE_YIELD;
}
if (!(qry->flags.FORWARD)) {
return KR_STATE_YIELD;
}
}
return KR_STATE_DONE;
}
/** Change ranks of RRs from this single iteration:
* _INITIAL or _TRY or _MISSING -> rank_to_set. Or any rank, if any_rank == true.
*
* Optionally do this only in a `bailiwick` (if not NULL).
* Iterator shouldn't have selected such records, but we check to be sure. */
static void rank_records(struct kr_query *qry, bool any_rank, enum kr_rank rank_to_set,
const knot_dname_t *bailiwick)
{
struct kr_request *req = qry->request;
ranked_rr_array_t *ptrs[2] = { &req->answ_selected, &req->auth_selected };
for (size_t i = 0; i < 2; ++i) {
ranked_rr_array_t *arr = ptrs[i];
for (size_t j = 0; j < arr->len; ++j) {
ranked_rr_array_entry_t *entry = arr->at[j];
if (entry->qry_uid != qry->uid) {
continue;
}
if (bailiwick && knot_dname_in_bailiwick(entry->rr->owner,
bailiwick) < 0) {
continue;
}
if (any_rank
|| kr_rank_test(entry->rank, KR_RANK_INITIAL)
|| kr_rank_test(entry->rank, KR_RANK_TRY)
|| kr_rank_test(entry->rank, KR_RANK_MISSING)) {
kr_rank_set(&entry->rank, rank_to_set);
}
}
}
}
static void check_wildcard(kr_layer_t *ctx)
{
struct kr_request *req = ctx->req;
struct kr_query *qry = req->current_query;
ranked_rr_array_t *ptrs[2] = { &req->answ_selected, &req->auth_selected };
for (int i = 0; i < 2; ++i) {
ranked_rr_array_t *arr = ptrs[i];
for (ssize_t j = 0; j < arr->len; ++j) {
ranked_rr_array_entry_t *entry = arr->at[j];
const knot_rrset_t *rrsigs = entry->rr;
if (qry->uid != entry->qry_uid) {
continue;
}
if (rrsigs->type != KNOT_RRTYPE_RRSIG) {
continue;
}
int owner_labels = knot_dname_labels(rrsigs->owner, NULL);
knot_rdata_t *rdata_k = rrsigs->rrs.rdata;
for (int k = 0; k < rrsigs->rrs.count;
++k, rdata_k = knot_rdataset_next(rdata_k)) {
if (knot_rrsig_labels(rdata_k) != owner_labels) {
qry->flags.DNSSEC_WEXPAND = true;
}
}
}
}
}
/** Just for wildcard_adjust_to_wire() */
static bool rr_is_for_wildcard(const ranked_rr_array_entry_t *entry)
{
switch (kr_rrset_type_maysig(entry->rr)) {
case KNOT_RRTYPE_NSEC:
case KNOT_RRTYPE_NSEC3:
return true;
default:
return false;
}
}
/** In case of wildcard expansion, mark required authority RRs by to_wire. */
static int wildcard_adjust_to_wire(struct kr_request *req, const struct kr_query *qry)
{
if (!qry->parent && qry->flags.DNSSEC_WEXPAND) {
return kr_ranked_rrarray_set_wire(&req->auth_selected, true,
qry->uid, true, &rr_is_for_wildcard);
}
return kr_ok();
}
static int validate(kr_layer_t *ctx, knot_pkt_t *pkt)
{
int ret = 0;
struct kr_request *req = ctx->req;
struct kr_query *qry = req->current_query;
if (qry->vld_limit_uid != qry->uid) {
qry->vld_limit_uid = qry->uid;
qry->vld_limit_crypto_remains = req->ctx->vld_limit_crypto;
}
/* Ignore faulty or unprocessed responses. */
if (ctx->state & (KR_STATE_FAIL|KR_STATE_CONSUME)) {
return ctx->state;
}
/* Pass-through if user doesn't want secure answer or stub. */
if (qry->flags.STUB) {
rank_records(qry, false, KR_RANK_OMIT, NULL);
return ctx->state;
}
uint8_t pkt_rcode = knot_wire_get_rcode(pkt->wire);
if ((qry->flags.FORWARD) &&
pkt_rcode != KNOT_RCODE_NOERROR &&
pkt_rcode != KNOT_RCODE_NXDOMAIN) {
do {
qry->flags.DNSSEC_BOGUS = true;
if (qry->cname_parent) {
qry->cname_parent->flags.DNSSEC_BOGUS = true;
}
qry = qry->parent;
} while (qry);
ctx->state = KR_STATE_DONE;
return ctx->state;
}
if (!(qry->flags.DNSSEC_WANT)) {
const bool is_insec = qry->flags.CACHED && qry->flags.DNSSEC_INSECURE;
if ((qry->flags.DNSSEC_INSECURE)) {
rank_records(qry, true, KR_RANK_INSECURE, qry->zone_cut.name);
}
if (is_insec && qry->parent != NULL) {
/* We have got insecure answer from cache.
* Mark parent(s) as insecure. */
mark_insecure_parents(qry);
VERBOSE_MSG(qry, "<= cached insecure response, going insecure\n");
ctx->state = KR_STATE_DONE;
} else if (ctx->state == KR_STATE_YIELD) {
/* Transition to insecure state
occurred during revalidation.
if state remains YIELD, answer will not be cached.
Let cache layers to work. */
ctx->state = KR_STATE_DONE;
}
return ctx->state;
}
/* Pass-through if CD bit is set. */
if (knot_wire_get_cd(req->qsource.packet->wire)) {
check_wildcard(ctx);
wildcard_adjust_to_wire(req, qry);
rank_records(qry, false, KR_RANK_OMIT, NULL);
return ctx->state;
}
/* Answer for RRSIG may not set DO=1, but all records MUST still validate. */
bool use_signatures = (knot_pkt_qtype(pkt) != KNOT_RRTYPE_RRSIG);
if (!(qry->flags.CACHED) && !knot_pkt_has_dnssec(pkt) && !use_signatures) {
VERBOSE_MSG(qry, "<= got insecure response\n");
kr_request_set_extended_error(req, KNOT_EDNS_EDE_BOGUS, "MISQ");
qry->flags.DNSSEC_BOGUS = true;
return KR_STATE_FAIL;
}
/* Check if this is a DNSKEY answer, check trust chain and store. */
uint16_t qtype = knot_pkt_qtype(pkt);
bool has_nsec3 = pkt_has_type(pkt, KNOT_RRTYPE_NSEC3);
const knot_pktsection_t *an = knot_pkt_section(pkt, KNOT_ANSWER);
const bool referral = (an->count == 0 && !knot_wire_get_aa(pkt->wire));
if (!(qry->flags.CACHED) && knot_wire_get_aa(pkt->wire)) {
/* Check if answer if not empty,
* but iterator has not selected any records. */
if (!check_empty_answer(ctx, pkt)) {
VERBOSE_MSG(qry, "<= no useful RR in authoritative answer\n");
kr_request_set_extended_error(req, KNOT_EDNS_EDE_BOGUS, "MJX6");
qry->flags.DNSSEC_BOGUS = true;
return KR_STATE_FAIL;
}
/* Track difference between current TA and signer name.
* This indicates that the NS is auth for both parent-child,
* and we must update DS/DNSKEY to validate it.
*/
ret = check_signer(ctx, pkt);
if (ret != KR_STATE_DONE) {
return ret;
}
if (qry->flags.FORWARD && qry->flags.DNSSEC_INSECURE) {
return KR_STATE_DONE;
}
}
/* Check for too many NSEC3 records. That's an issue, as some parts of validation
* are quadratic in their count, doing nontrivial computations inside.
* Also there seems to be no use in sending many NSEC3 records. */
if (!qry->flags.CACHED) {
const knot_pktsection_t *sec = knot_pkt_section(pkt, KNOT_AUTHORITY);
int count = 0;
for (int i = 0; i < sec->count; ++i)
count += (knot_pkt_rr(sec, i)->type == KNOT_RRTYPE_NSEC3);
if (count > 8) {
VERBOSE_MSG(qry, "<= too many NSEC3 records in AUTHORITY (%d)\n", count);
kr_request_set_extended_error(req, 27/*KNOT_EDNS_EDE_NSEC3_ITERS*/,
/* It's not about iteration values per se, but close enough. */
"DYRH: too many NSEC3 records");
qry->flags.DNSSEC_BOGUS = true;
return KR_STATE_FAIL;
}
}
if (knot_wire_get_aa(pkt->wire) && qtype == KNOT_RRTYPE_DNSKEY) {
const knot_rrset_t *ds = qry->zone_cut.trust_anchor;
if (ds && !kr_ds_algo_support(ds)) {
VERBOSE_MSG(qry, ">< all DS entries use unsupported algorithm pairs, going insecure\n");
/* ^ the message is a bit imprecise to avoid being too verbose */
kr_request_set_extended_error(req, KNOT_EDNS_EDE_OTHER, "LSLC: unsupported digest/key");
qry->flags.DNSSEC_WANT = false;
qry->flags.DNSSEC_INSECURE = true;
rank_records(qry, true, KR_RANK_INSECURE, qry->zone_cut.name);
mark_insecure_parents(qry);
return KR_STATE_DONE;
}
ret = validate_keyset(req, pkt, has_nsec3);
if (ret == kr_error(EAGAIN)) {
VERBOSE_MSG(qry, ">< cut changed, needs revalidation\n");
return KR_STATE_YIELD;
} else if (ret != 0) {
VERBOSE_MSG(qry, "<= bad keys, broken trust chain\n");
/* EDE code already set in validate_keyset() */
qry->flags.DNSSEC_BOGUS = true;
return KR_STATE_FAIL;
}
}
/* Validate all records, fail as bogus if it doesn't match.
* Do not revalidate data from cache, as it's already trusted.
* TTLs of RRsets may get lowered. */
if (!(qry->flags.CACHED)) {
ret = validate_records(req, pkt, req->rplan.pool, has_nsec3);
if (ret == KNOT_EDOWNGRADED) {
return KR_STATE_DONE;
} else if (ret == kr_error(E2BIG)) {
qry->flags.DNSSEC_BOGUS = true;
return KR_STATE_FAIL;
} else if (ret != 0) {
/* something exceptional - no DNS key, empty pointers etc
* normally it shouldn't happen */
VERBOSE_MSG(qry, "<= couldn't validate RRSIGs\n");
kr_request_set_extended_error(req, KNOT_EDNS_EDE_OTHER,
"O4TP: couldn't validate RRSIGs");
qry->flags.DNSSEC_BOGUS = true;
return KR_STATE_FAIL;
}
/* check validation state and spawn subrequests */
if (!req->answ_validated) {
ret = check_validation_result(ctx, pkt, &req->answ_selected);
if (ret != KR_STATE_DONE) {
return ret;
}
}
if (!req->auth_validated) {
ret = check_validation_result(ctx, pkt, &req->auth_selected);
if (ret != KR_STATE_DONE) {
return ret;
}
}
}
/* Validate non-existence proof if not positive answer.
* In case of CNAME, iterator scheduled a sibling query for the target,
* so we just drop the negative piece of information and don't try to prove it.
* TODO: not ideal; with aggressive cache we'll at least avoid the extra packet. */
if (!qry->flags.CACHED && pkt_rcode == KNOT_RCODE_NXDOMAIN && !qry->flags.CNAME) {
/* @todo If knot_pkt_qname(pkt) is used instead of qry->sname then the tests crash. */
if (!has_nsec3) {
ret = kr_nsec_negative(&req->auth_selected, qry->uid,
qry->sname, KNOT_RRTYPE_NULL);
if (ret >= 0) {
if (ret & PKT_NXDOMAIN) {
ret = kr_ok();
} else {
ret = kr_error(ENOENT); // probably proved NODATA
}
}
} else {
ret = kr_nsec3_name_error_response_check(pkt, KNOT_AUTHORITY, qry->sname);
}
if (has_nsec3 && (ret == kr_error(KNOT_ERANGE))) {
/* NXDOMAIN proof is OK,
* but NSEC3 that covers next closer name
* (or wildcard at next closer name) has opt-out flag.
* RFC5155 9.2; AD flag can not be set */
qry->flags.DNSSEC_OPTOUT = true;
VERBOSE_MSG(qry, "<= can't prove NXDOMAIN due to optout, going insecure\n");
} else if (ret != 0) {
VERBOSE_MSG(qry, "<= bad NXDOMAIN proof\n");
kr_request_set_extended_error(req, KNOT_EDNS_EDE_NSEC_MISS, "3WKM");
qry->flags.DNSSEC_BOGUS = true;
return KR_STATE_FAIL;
}
}
/* @todo WTH, this needs API that just tries to find a proof and the caller
* doesn't have to worry about NSEC/NSEC3
* @todo rework this
* CNAME: same as the NXDOMAIN case above */
if (!qry->flags.CACHED && pkt_rcode == KNOT_RCODE_NOERROR && !qry->flags.CNAME) {
bool no_data = (an->count == 0 && knot_wire_get_aa(pkt->wire));
if (no_data) {
/* @todo
* ? quick mechanism to determine which check to preform first
* ? merge the functionality together to share code/resources
*/
if (!has_nsec3) {
ret = kr_nsec_negative(&req->auth_selected, qry->uid,
knot_pkt_qname(pkt), knot_pkt_qtype(pkt));
if (ret >= 0) {
if (ret == PKT_NODATA) {
ret = kr_ok();
} else {
ret = kr_error(ENOENT); // suspicious
}
}
} else {
ret = kr_nsec3_no_data(pkt, KNOT_AUTHORITY, knot_pkt_qname(pkt), knot_pkt_qtype(pkt));
}
if (ret != 0) {
if (has_nsec3 && (ret == kr_error(KNOT_ERANGE))) {
VERBOSE_MSG(qry, "<= can't prove NODATA due to optout, going insecure\n");
qry->flags.DNSSEC_OPTOUT = true;
/* Could not return from here,
* we must continue, validate NSEC\NSEC3 and
* call update_parent_keys() to mark
* parent queries as insecure */
} else if (ret == KNOT_EDOWNGRADED) { // either NSEC3 or NSEC
VERBOSE_MSG(qry, "<= DNSSEC downgraded by a weird proof confusing NODATA with insecure delegation\n");
qry->flags.DNSSEC_WANT = false;
qry->flags.DNSSEC_INSECURE = true;
rank_records(qry, true, KR_RANK_INSECURE, qry->sname);
mark_insecure_parents(qry);
} else {
VERBOSE_MSG(qry, "<= bad NODATA proof\n");
kr_request_set_extended_error(req, KNOT_EDNS_EDE_NSEC_MISS, "AHXI");
qry->flags.DNSSEC_BOGUS = true;
return KR_STATE_FAIL;
}
}
}
}
wildcard_adjust_to_wire(req, qry);
/* Check and update current delegation point security status. */
ret = update_delegation(req, qry, pkt, has_nsec3);
if (ret == DNSSEC_NOT_FOUND && qry->stype != KNOT_RRTYPE_DS) {
if (ctx->state == KR_STATE_YIELD) {
VERBOSE_MSG(qry, "<= can't validate referral\n");
kr_request_set_extended_error(req, KNOT_EDNS_EDE_BOGUS, "XLE4");
qry->flags.DNSSEC_BOGUS = true;
return KR_STATE_FAIL;
} else {
/* Check the trust chain and query DS\DNSKEY if needed. */
VERBOSE_MSG(qry, "<= DS\\NSEC was not found, querying for DS\n");
return KR_STATE_YIELD;
}
} else if (ret != 0) {
return KR_STATE_FAIL;
} else if (pkt_rcode == KNOT_RCODE_NOERROR &&
referral &&
((!qry->flags.DNSSEC_WANT && qry->flags.DNSSEC_INSECURE) ||
(qry->flags.DNSSEC_NODS))) {
/* referral with proven DS non-existence */
qtype = KNOT_RRTYPE_DS;
}
/* Update parent query zone cut */
if (qry->parent) {
if (update_parent_keys(req, qtype) != 0) {
return KR_STATE_FAIL;
}
}
if (qry->flags.FORWARD && qry->parent) {
if (pkt_rcode == KNOT_RCODE_NXDOMAIN) {
qry->parent->forward_flags.NO_MINIMIZE = true;
}
}
VERBOSE_MSG(qry, "<= answer valid, OK\n");
return KR_STATE_DONE;
}
/** Hide RRsets which did not validate from clients. */
static int hide_bogus(kr_layer_t *ctx) {
if (knot_wire_get_cd(ctx->req->qsource.packet->wire)) {
return ctx->state;
}
/* We don't want to send bogus answers to clients, not even in SERVFAIL
* answers, but we cannot drop whole sections. If a CNAME chain
* SERVFAILs somewhere, the steps that were OK should be put into
* answer.
*
* There is one specific issue: currently we follow CNAME *before*
* we validate it, because... iterator comes before validator.
* Therefore some rrsets might be added into req->*_selected before
* we detected failure in validator.
* TODO: better approach, probably during work on parallel queries.
*/
const ranked_rr_array_t *sel[] = kr_request_selected(ctx->req);
for (knot_section_t sect = KNOT_ANSWER; sect <= KNOT_ADDITIONAL; ++sect) {
for (size_t i = 0; i < sel[sect]->len; ++i) {
ranked_rr_array_entry_t *e = sel[sect]->at[i];
e->to_wire = e->to_wire
&& !kr_rank_test(e->rank, KR_RANK_INDET)
&& !kr_rank_test(e->rank, KR_RANK_BOGUS)
&& !kr_rank_test(e->rank, KR_RANK_MISMATCH)
&& !kr_rank_test(e->rank, KR_RANK_MISSING);
}
}
return ctx->state;
}
static int validate_wrapper(kr_layer_t *ctx, knot_pkt_t *pkt) {
// Wrapper for now.
int ret = validate(ctx, pkt);
struct kr_request *req = ctx->req;
struct kr_query *qry = req->current_query;
if (ret & KR_STATE_FAIL && qry->flags.DNSSEC_BOGUS)
qry->server_selection.error(qry, req->upstream.transport, KR_SELECTION_DNSSEC_ERROR);
if (ret & KR_STATE_DONE && !qry->flags.DNSSEC_BOGUS) {
/* Don't report extended DNS errors related to validation
* when it managed to succeed (e.g. by trying different auth). */
switch (req->extended_error.info_code) {
case KNOT_EDNS_EDE_BOGUS:
case KNOT_EDNS_EDE_NSEC_MISS:
case KNOT_EDNS_EDE_RRSIG_MISS:
case KNOT_EDNS_EDE_SIG_EXPIRED:
case KNOT_EDNS_EDE_SIG_NOTYET:
kr_request_set_extended_error(req, KNOT_EDNS_EDE_NONE, NULL);
break;
case KNOT_EDNS_EDE_DNSKEY_MISS:
case KNOT_EDNS_EDE_DNSKEY_BIT:
kr_assert(false); /* These EDE codes aren't used. */
break;
default: break; /* Remaining codes don't indicate hard DNSSEC failure. */
}
}
return ret;
}
/** Module implementation. */
int validate_init(struct kr_module *self)
{
static const kr_layer_api_t layer = {
.consume = &validate_wrapper,
.answer_finalize = &hide_bogus,
};
self->layer = &layer;
return kr_ok();
}
KR_MODULE_EXPORT(validate) /* useless for builtin module, but let's be consistent */
#undef VERBOSE_MSG