1400 lines
46 KiB
C
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
|