summaryrefslogtreecommitdiffstats
path: root/lib/dnssec/nsec.c
blob: 8b1724786e3d1209a4dfc2f0d4a318e0e0da5f2e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
/*  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 "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);

	int res = 0;
	while (res == 0 && d1_rev != NULL) {
		res = lf_cmp(d1_rev, d2_rev);
		d1_rev = knot_wire_next_label(d1_rev, NULL);
		d2_rev = knot_wire_next_label(d2_rev, NULL);
	}

	kr_require(res != 0 || d2_rev == NULL);
	return res;
}


/**
 * 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. */
		if (dnssec_nsec_bitmap_contains(bm, bm_size, KNOT_RRTYPE_NS)
		    && !dnssec_nsec_bitmap_contains(bm, bm_size, KNOT_RRTYPE_SOA)) {
			return NO_PROOF;
		}
		/* 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)
		    && no_data_response_check_rrtype(nsec, stype) == 0) {
			return PKT_NODATA; // proven NODATA by matching NSEC
		}
		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_wire_next_label(clencl, NULL);
	(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);
	}
}