322 lines
11 KiB
C
322 lines
11 KiB
C
/* Copyright (C) CZ.NIC, z.s.p.o. <knot-resolver@labs.nic.cz>
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*/
|
|
|
|
#include <stdlib.h>
|
|
|
|
#include <libknot/descriptor.h>
|
|
#include <libknot/dname.h>
|
|
#include <libknot/packet/wire.h>
|
|
#include <libknot/rrset.h>
|
|
#include <libknot/rrtype/nsec.h>
|
|
#include <libknot/rrtype/rrsig.h>
|
|
#include <libdnssec/error.h>
|
|
#include <libdnssec/nsec.h>
|
|
|
|
#include "lib/defines.h"
|
|
#include "lib/dnssec/nsec.h"
|
|
#include "lib/utils.h"
|
|
#include "lib/resolve.h"
|
|
|
|
int kr_nsec_children_in_zone_check(const uint8_t *bm, uint16_t bm_size)
|
|
{
|
|
if (kr_fails_assert(bm))
|
|
return kr_error(EINVAL);
|
|
const bool parent_side =
|
|
dnssec_nsec_bitmap_contains(bm, bm_size, KNOT_RRTYPE_DNAME)
|
|
|| (dnssec_nsec_bitmap_contains(bm, bm_size, KNOT_RRTYPE_NS)
|
|
&& !dnssec_nsec_bitmap_contains(bm, bm_size, KNOT_RRTYPE_SOA)
|
|
);
|
|
return parent_side ? abs(ENOENT) : kr_ok();
|
|
/* LATER: after refactoring, probably also check if signer name equals owner,
|
|
* but even without that it's not possible to attack *correctly* signed zones.
|
|
*/
|
|
}
|
|
|
|
/* This block of functions implements a "safe" version of knot_dname_cmp(),
|
|
* until that one handles in-label zero bytes correctly. */
|
|
static int lf_cmp(const uint8_t *lf1, const uint8_t *lf2)
|
|
{
|
|
/* Compare common part. */
|
|
uint8_t common = lf1[0];
|
|
if (common > lf2[0]) {
|
|
common = lf2[0];
|
|
}
|
|
int ret = memcmp(lf1 + 1, lf2 + 1, common);
|
|
if (ret != 0) {
|
|
return ret;
|
|
}
|
|
|
|
/* If they match, compare lengths. */
|
|
if (lf1[0] < lf2[0]) {
|
|
return -1;
|
|
} else if (lf1[0] > lf2[0]) {
|
|
return 1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
static void dname_reverse(const knot_dname_t *src, size_t src_len, knot_dname_t *dst)
|
|
{
|
|
knot_dname_t *idx = dst + src_len - 1;
|
|
kr_require(src[src_len - 1] == '\0');
|
|
*idx = '\0';
|
|
|
|
while (*src) {
|
|
uint16_t len = *src + 1;
|
|
idx -= len;
|
|
memcpy(idx, src, len);
|
|
src += len;
|
|
}
|
|
kr_require(idx == dst);
|
|
}
|
|
static int dname_cmp(const knot_dname_t *d1, const knot_dname_t *d2)
|
|
{
|
|
size_t d1_len = knot_dname_size(d1);
|
|
size_t d2_len = knot_dname_size(d2);
|
|
|
|
knot_dname_t d1_rev_arr[d1_len], d2_rev_arr[d2_len];
|
|
const knot_dname_t *d1_rev = d1_rev_arr, *d2_rev = d2_rev_arr;
|
|
|
|
dname_reverse(d1, d1_len, d1_rev_arr);
|
|
dname_reverse(d2, d2_len, d2_rev_arr);
|
|
|
|
do {
|
|
int res = lf_cmp(d1_rev, d2_rev);
|
|
if (res != 0 || d1_rev[0] == '\0')
|
|
return res;
|
|
d1_rev = knot_dname_next_label(d1_rev);
|
|
d2_rev = knot_dname_next_label(d2_rev);
|
|
} while (true);
|
|
}
|
|
|
|
|
|
/**
|
|
* Check whether this nsec proves that there is no closer match for sname.
|
|
*
|
|
* @param nsec NSEC RRSet.
|
|
* @param sname Searched name.
|
|
* @return 0 if proves, >0 if not (abs(ENOENT) or abs(EEXIST)), or error code (<0).
|
|
*/
|
|
static int nsec_covers(const knot_rrset_t *nsec, const knot_dname_t *sname)
|
|
{
|
|
if (kr_fails_assert(nsec && sname))
|
|
return kr_error(EINVAL);
|
|
const int cmp = dname_cmp(sname, nsec->owner);
|
|
if (cmp < 0) return abs(ENOENT); /* 'sname' before 'owner', so can't be covered */
|
|
if (cmp == 0) return abs(EEXIST); /* matched, not covered */
|
|
|
|
/* We have to lower-case 'next' with libknot >= 2.7; see also RFC 6840 5.1. */
|
|
knot_dname_t next[KNOT_DNAME_MAXLEN];
|
|
int ret = knot_dname_to_wire(next, knot_nsec_next(nsec->rrs.rdata), sizeof(next));
|
|
if (kr_fails_assert(ret >= 0))
|
|
return kr_error(ret);
|
|
knot_dname_to_lower(next);
|
|
|
|
/* If NSEC 'owner' >= 'next', it means that there is nothing after 'owner' */
|
|
const bool is_last_nsec = dname_cmp(nsec->owner, next) >= 0;
|
|
const bool in_range = is_last_nsec || dname_cmp(sname, next) < 0;
|
|
if (!in_range)
|
|
return abs(ENOENT);
|
|
/* Before returning kr_ok(), we have to check a special case:
|
|
* sname might be under delegation from owner and thus
|
|
* not in the zone of this NSEC at all.
|
|
*/
|
|
if (knot_dname_in_bailiwick(sname, nsec->owner) <= 0)
|
|
return kr_ok();
|
|
const uint8_t *bm = knot_nsec_bitmap(nsec->rrs.rdata);
|
|
uint16_t bm_size = knot_nsec_bitmap_len(nsec->rrs.rdata);
|
|
|
|
return kr_nsec_children_in_zone_check(bm, bm_size);
|
|
}
|
|
|
|
int kr_nsec_bitmap_nodata_check(const uint8_t *bm, uint16_t bm_size, uint16_t type, const knot_dname_t *owner)
|
|
{
|
|
const int NO_PROOF = abs(ENOENT);
|
|
if (!bm || !owner)
|
|
return kr_error(EINVAL);
|
|
if (dnssec_nsec_bitmap_contains(bm, bm_size, type))
|
|
return NO_PROOF;
|
|
|
|
if (type != KNOT_RRTYPE_CNAME
|
|
&& dnssec_nsec_bitmap_contains(bm, bm_size, KNOT_RRTYPE_CNAME)) {
|
|
return NO_PROOF;
|
|
}
|
|
/* Special behavior around zone cuts. */
|
|
switch (type) {
|
|
case KNOT_RRTYPE_DS:
|
|
/* Security feature: in case of DS also check for SOA
|
|
* non-existence to be more certain that we don't hold
|
|
* a child-side NSEC by some mistake (e.g. when forwarding).
|
|
* See RFC4035 5.2, next-to-last paragraph.
|
|
* This doesn't apply for root DS as it doesn't exist in DNS hierarchy.
|
|
*/
|
|
if (owner[0] != '\0' && dnssec_nsec_bitmap_contains(bm, bm_size, KNOT_RRTYPE_SOA))
|
|
return NO_PROOF;
|
|
break;
|
|
case KNOT_RRTYPE_CNAME:
|
|
/* Exception from the `default` rule. It's perhaps disputable,
|
|
* but existence of CNAME at zone apex is not allowed, so we
|
|
* consider a parent-side record to be enough to prove non-existence. */
|
|
break;
|
|
default:
|
|
/* Parent-side delegation record isn't authoritative for non-DS;
|
|
* see RFC6840 4.1.
|
|
*
|
|
* Additionally, we signal if the NODATA would belong
|
|
* to an *insecure* child zone.
|
|
*/
|
|
if (dnssec_nsec_bitmap_contains(bm, bm_size, KNOT_RRTYPE_NS)
|
|
&& !dnssec_nsec_bitmap_contains(bm, bm_size, KNOT_RRTYPE_SOA)) {
|
|
return dnssec_nsec_bitmap_contains(bm, bm_size, KNOT_RRTYPE_DS)
|
|
? NO_PROOF
|
|
: KNOT_EDOWNGRADED;
|
|
}
|
|
/* LATER(opt): perhaps short-circuit test if we repeat it here. */
|
|
}
|
|
|
|
return kr_ok();
|
|
}
|
|
|
|
/// Convenience wrapper for kr_nsec_bitmap_nodata_check()
|
|
static int no_data_response_check_rrtype(const knot_rrset_t *nsec, uint16_t type)
|
|
{
|
|
if (kr_fails_assert(nsec && nsec->type == KNOT_RRTYPE_NSEC))
|
|
return kr_error(EINVAL);
|
|
const uint8_t *bm = knot_nsec_bitmap(nsec->rrs.rdata);
|
|
uint16_t bm_size = knot_nsec_bitmap_len(nsec->rrs.rdata);
|
|
return kr_nsec_bitmap_nodata_check(bm, bm_size, type, nsec->owner);
|
|
}
|
|
|
|
int kr_nsec_wildcard_answer_response_check(const knot_pkt_t *pkt, knot_section_t section_id,
|
|
const knot_dname_t *sname)
|
|
{
|
|
const knot_pktsection_t *sec = knot_pkt_section(pkt, section_id);
|
|
if (!sec || !sname)
|
|
return kr_error(EINVAL);
|
|
|
|
for (unsigned i = 0; i < sec->count; ++i) {
|
|
const knot_rrset_t *rrset = knot_pkt_rr(sec, i);
|
|
if (rrset->type != KNOT_RRTYPE_NSEC)
|
|
continue;
|
|
if (nsec_covers(rrset, sname) == 0)
|
|
return kr_ok();
|
|
}
|
|
|
|
return kr_error(ENOENT);
|
|
}
|
|
|
|
int kr_nsec_negative(const ranked_rr_array_t *rrrs, uint32_t qry_uid,
|
|
const knot_dname_t *sname, uint16_t stype)
|
|
{
|
|
// We really only consider the (canonically) first NSEC in each RRset.
|
|
// Using same owner with differing content probably isn't useful for NSECs anyway.
|
|
// Many other parts of code do the same, too.
|
|
if (kr_fails_assert(rrrs && sname))
|
|
return kr_error(EINVAL);
|
|
|
|
// Terminology: https://datatracker.ietf.org/doc/html/rfc4592#section-3.3.1
|
|
int clencl_labels = -1; // the label count of the closest encloser of sname
|
|
for (int i = rrrs->len - 1; i >= 0; --i) { // NSECs near the end typically
|
|
const knot_rrset_t *nsec = rrrs->at[i]->rr;
|
|
bool ok = rrrs->at[i]->qry_uid == qry_uid
|
|
&& nsec->type == KNOT_RRTYPE_NSEC
|
|
&& kr_rank_test(rrrs->at[i]->rank, KR_RANK_SECURE);
|
|
if (!ok) continue;
|
|
const int covers = nsec_covers(nsec, sname);
|
|
if (covers == abs(EEXIST)) {
|
|
int ret = no_data_response_check_rrtype(nsec, stype);
|
|
if (ret == 0)
|
|
return PKT_NODATA; // proven NODATA by matching NSEC
|
|
if (ret == KNOT_EDOWNGRADED)
|
|
return ret;
|
|
}
|
|
if (covers != 0) continue;
|
|
|
|
// We have to lower-case 'next' with libknot >= 2.7; see also RFC 6840 5.1.
|
|
// LATER(optim.): it's duplicate work with the nsec_covers() call.
|
|
knot_dname_t next[KNOT_DNAME_MAXLEN];
|
|
int ret = knot_dname_to_wire(next, knot_nsec_next(nsec->rrs.rdata), sizeof(next));
|
|
if (kr_fails_assert(ret >= 0))
|
|
return kr_error(ret);
|
|
knot_dname_to_lower(next);
|
|
|
|
clencl_labels = MAX(knot_dname_matched_labels(nsec->owner, sname),
|
|
knot_dname_matched_labels(sname, next));
|
|
break; // reduce indentation again
|
|
}
|
|
|
|
if (clencl_labels < 0)
|
|
return kr_error(ENOENT);
|
|
const int sname_labels = knot_dname_labels(sname, NULL);
|
|
if (sname_labels == clencl_labels)
|
|
return PKT_NODATA; // proven NODATA; sname is an empty non-terminal
|
|
|
|
// Explicitly construct name for the corresponding source of synthesis.
|
|
knot_dname_t ssynth[KNOT_DNAME_MAXLEN + 2];
|
|
ssynth[0] = 1;
|
|
ssynth[1] = '*';
|
|
const knot_dname_t *clencl = sname;
|
|
for (int l = sname_labels; l > clencl_labels; --l)
|
|
clencl = knot_dname_next_label(clencl);
|
|
(void)!!knot_dname_store(&ssynth[2], clencl);
|
|
|
|
// Try to (dis)prove the source of synthesis by a covering or matching NSEC.
|
|
for (int i = rrrs->len - 1; i >= 0; --i) { // NSECs near the end typically
|
|
const knot_rrset_t *nsec = rrrs->at[i]->rr;
|
|
bool ok = rrrs->at[i]->qry_uid == qry_uid
|
|
&& nsec->type == KNOT_RRTYPE_NSEC
|
|
&& kr_rank_test(rrrs->at[i]->rank, KR_RANK_SECURE);
|
|
if (!ok) continue;
|
|
const int covers = nsec_covers(nsec, ssynth);
|
|
if (covers == abs(EEXIST)) {
|
|
int ret = no_data_response_check_rrtype(nsec, stype);
|
|
if (ret == 0) return PKT_NODATA; // proven NODATA by wildcard NSEC
|
|
// TODO: also try expansion? Or at least a different return code?
|
|
} else if (covers == 0) {
|
|
return PKT_NXDOMAIN | PKT_NODATA;
|
|
}
|
|
}
|
|
return kr_error(ENOENT);
|
|
}
|
|
|
|
int kr_nsec_ref_to_unsigned(const ranked_rr_array_t *rrrs, uint32_t qry_uid,
|
|
const knot_dname_t *sname)
|
|
{
|
|
for (int i = rrrs->len - 1; i >= 0; --i) { // NSECs near the end typically
|
|
const knot_rrset_t *nsec = rrrs->at[i]->rr;
|
|
bool ok = rrrs->at[i]->qry_uid == qry_uid
|
|
&& nsec->type == KNOT_RRTYPE_NSEC
|
|
&& kr_rank_test(rrrs->at[i]->rank, KR_RANK_SECURE)
|
|
// avoid any possibility of getting tricked in deeper zones
|
|
&& knot_dname_in_bailiwick(sname, nsec->owner) >= 0;
|
|
if (!ok) continue;
|
|
|
|
kr_assert(nsec->rrs.rdata);
|
|
const uint8_t *bm = knot_nsec_bitmap(nsec->rrs.rdata);
|
|
uint16_t bm_size = knot_nsec_bitmap_len(nsec->rrs.rdata);
|
|
ok = ok && dnssec_nsec_bitmap_contains(bm, bm_size, KNOT_RRTYPE_NS)
|
|
&& !dnssec_nsec_bitmap_contains(bm, bm_size, KNOT_RRTYPE_DS)
|
|
&& !dnssec_nsec_bitmap_contains(bm, bm_size, KNOT_RRTYPE_SOA);
|
|
if (ok) return kr_ok();
|
|
}
|
|
return kr_error(DNSSEC_NOT_FOUND);
|
|
}
|
|
|
|
int kr_nsec_matches_name_and_type(const knot_rrset_t *nsec,
|
|
const knot_dname_t *name, uint16_t type)
|
|
{
|
|
/* It's not secure enough to just check a single bit for (some) other types,
|
|
* but we (currently) only use this API for NS. See RFC 6840 sec. 4. */
|
|
if (kr_fails_assert(type == KNOT_RRTYPE_NS && nsec && nsec->rrs.rdata && name))
|
|
return kr_error(EINVAL);
|
|
if (!knot_dname_is_equal(nsec->owner, name))
|
|
return kr_error(ENOENT);
|
|
const uint8_t *bm = knot_nsec_bitmap(nsec->rrs.rdata);
|
|
uint16_t bm_size = knot_nsec_bitmap_len(nsec->rrs.rdata);
|
|
if (dnssec_nsec_bitmap_contains(bm, bm_size, type)) {
|
|
return kr_ok();
|
|
} else {
|
|
return kr_error(ENOENT);
|
|
}
|
|
}
|