diff options
Diffstat (limited to 'lib/dnssec')
-rw-r--r-- | lib/dnssec/nsec.c | 315 | ||||
-rw-r--r-- | lib/dnssec/nsec.h | 69 | ||||
-rw-r--r-- | lib/dnssec/nsec3.c | 734 | ||||
-rw-r--r-- | lib/dnssec/nsec3.h | 116 | ||||
-rw-r--r-- | lib/dnssec/signature.c | 304 | ||||
-rw-r--r-- | lib/dnssec/signature.h | 29 | ||||
-rw-r--r-- | lib/dnssec/ta.c | 154 | ||||
-rw-r--r-- | lib/dnssec/ta.h | 61 |
8 files changed, 1782 insertions, 0 deletions
diff --git a/lib/dnssec/nsec.c b/lib/dnssec/nsec.c new file mode 100644 index 0000000..8b17247 --- /dev/null +++ b/lib/dnssec/nsec.c @@ -0,0 +1,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); + } +} diff --git a/lib/dnssec/nsec.h b/lib/dnssec/nsec.h new file mode 100644 index 0000000..a173fa5 --- /dev/null +++ b/lib/dnssec/nsec.h @@ -0,0 +1,69 @@ +/* Copyright (C) CZ.NIC, z.s.p.o. <knot-resolver@labs.nic.cz> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include <libknot/packet/pkt.h> + +#include "lib/layer/iterate.h" + +/** + * Check bitmap that child names are contained in the same zone. + * @note see RFC6840 4.1. + * @param bm Bitmap from NSEC or NSEC3. + * @param bm_size Bitmap size. + * @return 0 if they are, >0 if not (abs(ENOENT)), <0 on error. + */ +int kr_nsec_children_in_zone_check(const uint8_t *bm, uint16_t bm_size); + +/** + * Check an NSEC or NSEC3 bitmap for NODATA for a type. + * @param bm Bitmap. + * @param bm_size Bitmap size. + * @param type RR type to check. + * @param owner NSEC record owner. + * @note This includes special checks for zone cuts, e.g. from RFC 6840 sec. 4. + * @return 0, abs(ENOENT) (no proof), kr_error(EINVAL) + */ +int kr_nsec_bitmap_nodata_check(const uint8_t *bm, uint16_t bm_size, uint16_t type, const knot_dname_t *owner); + +/** + * Wildcard answer response check (RFC4035 3.1.3.3). + * @param pkt Packet structure to be processed. + * @param section_id Packet section to be processed. + * @param sname Name to be checked. + * @return 0 or error code. + */ +int kr_nsec_wildcard_answer_response_check(const knot_pkt_t *pkt, knot_section_t section_id, + const knot_dname_t *sname); + +/** + * Search for a negative proof for sname+stype among validated NSECs. + * + * @param rrrs list of RRs to search; typically kr_request::auth_selected + * @param qry_uid only consider NSECs from this packet, for better efficiency + * @return negative error code, or PKT_NXDOMAIN | PKT_NODATA (both for NXDOMAIN) + */ +int kr_nsec_negative(const ranked_rr_array_t *rrrs, uint32_t qry_uid, + const knot_dname_t *sname, uint16_t stype); + +/** + * Referral to unsigned subzone check (RFC4035 5.2). + * + * @param rrrs list of RRs to search; typically kr_request::auth_selected + * @param qry_uid only consider NSECs from this packet, for better efficiency + * @return 0 or negative error code, in particular DNSSEC_NOT_FOUND + */ +int kr_nsec_ref_to_unsigned(const ranked_rr_array_t *rrrs, uint32_t qry_uid, + const knot_dname_t *sname); + +/** + * Checks whether supplied NSEC RR matches the supplied name and type. + * @param nsec NSEC RR. + * @param name Name to be checked. + * @param type Type to be checked. Only use with NS! TODO (+copy&paste NSEC3) + * @return 0 or error code. + */ +int kr_nsec_matches_name_and_type(const knot_rrset_t *nsec, + const knot_dname_t *name, uint16_t type); diff --git a/lib/dnssec/nsec3.c b/lib/dnssec/nsec3.c new file mode 100644 index 0000000..4199f25 --- /dev/null +++ b/lib/dnssec/nsec3.c @@ -0,0 +1,734 @@ +/* Copyright (C) CZ.NIC, z.s.p.o. <knot-resolver@labs.nic.cz> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include <string.h> + +#include <libdnssec/binary.h> +#include <libdnssec/error.h> +#include <libdnssec/nsec.h> +#include <libknot/descriptor.h> +#include <contrib/base32hex.h> +#include <libknot/rrset.h> +#include <libknot/rrtype/nsec3.h> + +#include "lib/defines.h" +#include "lib/dnssec/nsec.h" +#include "lib/dnssec/nsec3.h" +#include "lib/utils.h" + +#define OPT_OUT_BIT 0x01 + +//#define FLG_CLOSEST_ENCLOSER (1 << 0) +#define FLG_CLOSEST_PROVABLE_ENCLOSER (1 << 1) +#define FLG_NAME_COVERED (1 << 2) +#define FLG_NAME_MATCHED (1 << 3) +#define FLG_TYPE_BIT_MISSING (1 << 4) +#define FLG_CNAME_BIT_MISSING (1 << 5) + +/** + * Obtains NSEC3 parameters from RR. + * @param params NSEC3 parameters structure to be set. + * @param nsec3 NSEC3 RR containing the parameters. + * @return 0 or error code. + */ +static int nsec3_parameters(dnssec_nsec3_params_t *params, const knot_rrset_t *nsec3) +{ + if (kr_fails_assert(params && nsec3)) + return kr_error(EINVAL); + + const knot_rdata_t *rr = knot_rdataset_at(&nsec3->rrs, 0); + if (kr_fails_assert(rr)) + return kr_error(EINVAL); + + /* Every NSEC3 RR contains data from NSEC3PARAMS. */ + const size_t SALT_OFFSET = 5; /* First 5 octets contain { Alg, Flags, Iterations, Salt length } */ + dnssec_binary_t rdata = { + .size = SALT_OFFSET + (size_t)knot_nsec3_salt_len(nsec3->rrs.rdata), + .data = /*const-cast*/(uint8_t *)rr->data, + }; + if (rdata.size > rr->len) + return kr_error(EMSGSIZE); + + int ret = dnssec_nsec3_params_from_rdata(params, &rdata); + if (ret != DNSSEC_EOK) + return kr_error(EINVAL); + + return kr_ok(); +} + +/** + * Computes a hash of a given domain name. + * @param hash Resulting hash, must be freed. + * @param params NSEC3 parameters. + * @param name Domain name to be hashed. + * @return 0 or error code. + */ +static int hash_name(dnssec_binary_t *hash, const dnssec_nsec3_params_t *params, + const knot_dname_t *name) +{ + if (kr_fails_assert(hash && params)) + return kr_error(EINVAL); + if (!name) + return kr_error(EINVAL); + if (kr_fails_assert(!kr_nsec3_limited_params(params))) { + /* This if is mainly defensive; it shouldn't happen. */ + return kr_error(EINVAL); + } + + dnssec_binary_t dname = { + .size = knot_dname_size(name), + .data = (uint8_t *) name, + }; + + int ret = dnssec_nsec3_hash(&dname, params, hash); + if (ret != DNSSEC_EOK) { + return kr_error(EINVAL); + } + + return kr_ok(); +} + +/** + * Read hash from NSEC3 owner name and store its binary form. + * @param hash Buffer to be written. + * @param max_hash_size Maximal has size. + * @param nsec3 NSEC3 RR. + * @return 0 or error code. + */ +static int read_owner_hash(dnssec_binary_t *hash, size_t max_hash_size, const knot_rrset_t *nsec3) +{ + if (kr_fails_assert(hash && nsec3 && hash->data)) + return kr_error(EINVAL); + + int32_t ret = base32hex_decode(nsec3->owner + 1, nsec3->owner[0], hash->data, max_hash_size); + if (ret < 0) + return kr_error(EILSEQ); + hash->size = ret; + + return kr_ok(); +} + +#define MAX_HASH_BYTES 64 +/** + * Closest (provable) encloser match (RFC5155 7.2.1, bullet 1). + * @param flags Flags to be set according to check outcome. + * @param nsec3 NSEC3 RR. + * @param name Name to be checked. + * @param skipped Number of skipped labels to find closest (provable) match. + * @return 0 or error code. + */ +static int closest_encloser_match(int *flags, const knot_rrset_t *nsec3, + const knot_dname_t *name, unsigned *skipped) +{ + if (kr_fails_assert(flags && nsec3 && name && skipped)) + return kr_error(EINVAL); + + uint8_t hash_data[MAX_HASH_BYTES] = {0, }; + dnssec_binary_t owner_hash = { 0, hash_data }; + dnssec_nsec3_params_t params = { 0, }; + dnssec_binary_t name_hash = { 0, }; + + int ret = read_owner_hash(&owner_hash, MAX_HASH_BYTES, nsec3); + if (ret != 0) + goto fail; + + ret = nsec3_parameters(¶ms, nsec3); + if (ret != 0) + goto fail; + + /* Root label has no encloser */ + if (!name[0]) { + ret = kr_error(ENOENT); + goto fail; + } + + const knot_dname_t *encloser = knot_wire_next_label(name, NULL); + *skipped = 1; + + /* Avoid doing too much work on SHA1, mitigating: + * CVE-2023-50868: NSEC3 closest encloser proof can exhaust CPU + * We log nothing here; it wouldn't be easy from this place + * and huge SNAME should be suspicious on its own. + */ + const int max_labels = knot_dname_labels(nsec3->owner, NULL) - 1 + + kr_nsec3_max_depth(¶ms); + for (int l = knot_dname_labels(encloser, NULL); l > max_labels; --l) { + encloser = knot_wire_next_label(encloser, NULL); + ++(*skipped); + } + + while(encloser) { + ret = hash_name(&name_hash, ¶ms, encloser); + if (ret != 0) + goto fail; + + if ((owner_hash.size == name_hash.size) && + (memcmp(owner_hash.data, name_hash.data, owner_hash.size) == 0)) { + dnssec_binary_free(&name_hash); + *flags |= FLG_CLOSEST_PROVABLE_ENCLOSER; + break; + } + + dnssec_binary_free(&name_hash); + + if (!encloser[0]) + break; + encloser = knot_wire_next_label(encloser, NULL); + ++(*skipped); + } + + ret = kr_ok(); + +fail: + if (params.salt.data) + dnssec_nsec3_params_free(¶ms); + if (name_hash.data) + dnssec_binary_free(&name_hash); + return ret; +} + +/** + * Checks whether NSEC3 RR covers the supplied name (RFC5155 7.2.1, bullet 2). + * @param flags Flags to be set according to check outcome. + * @param nsec3 NSEC3 RR. + * @param name Name to be checked. + * @return 0 or error code. + */ +static int covers_name(int *flags, const knot_rrset_t *nsec3, const knot_dname_t *name) +{ + if (kr_fails_assert(flags && nsec3 && name)) + return kr_error(EINVAL); + + uint8_t hash_data[MAX_HASH_BYTES] = { 0, }; + dnssec_binary_t owner_hash = { 0, hash_data }; + dnssec_nsec3_params_t params = { 0, }; + dnssec_binary_t name_hash = { 0, }; + + int ret = read_owner_hash(&owner_hash, MAX_HASH_BYTES, nsec3); + if (ret != 0) + goto fail; + + ret = nsec3_parameters(¶ms, nsec3); + if (ret != 0) + goto fail; + + ret = hash_name(&name_hash, ¶ms, name); + if (ret != 0) + goto fail; + + uint8_t next_size = knot_nsec3_next_len(nsec3->rrs.rdata); + const uint8_t *next_hash = knot_nsec3_next(nsec3->rrs.rdata); + + if ((next_size > 0) && (owner_hash.size == next_size) && (name_hash.size == next_size)) { + /* All hash lengths must be same. */ + const uint8_t *ownerd = owner_hash.data; + const uint8_t *nextd = next_hash; + int covered = 0; + int greater_then_owner = (memcmp(ownerd, name_hash.data, next_size) < 0); + int less_then_next = (memcmp(name_hash.data, nextd, next_size) < 0); + if (memcmp(ownerd, nextd, next_size) < 0) { + /* + * 0 (...) owner ... next (...) MAX + * ^ + * name + * ==> + * (owner < name) && (name < next) + */ + covered = ((greater_then_owner) && (less_then_next)); + } else { + /* + * owner ... MAX, 0 ... next + * ^ ^ ^ + * name name name + * => + * (owner < name) || (name < next) + */ + covered = ((greater_then_owner) || (less_then_next)); + } + + if (covered) { + *flags |= FLG_NAME_COVERED; + + uint8_t nsec3_flags = knot_nsec3_flags(nsec3->rrs.rdata); + if (nsec3_flags & ~OPT_OUT_BIT) { + /* RFC5155 3.1.2 */ + ret = kr_error(EINVAL); + } else { + ret = kr_ok(); + } + } + } + +fail: + if (params.salt.data) + dnssec_nsec3_params_free(¶ms); + if (name_hash.data) + dnssec_binary_free(&name_hash); + return ret; +} + +/** + * Checks whether NSEC3 RR has the opt-out bit set. + * @param flags Flags to be set according to check outcome. + * @param nsec3 NSEC3 RR. + * @param name Name to be checked. + * @return 0 or error code. + */ +static bool has_optout(const knot_rrset_t *nsec3) +{ + if (!nsec3) + return false; + + uint8_t nsec3_flags = knot_nsec3_flags(nsec3->rrs.rdata); + if (nsec3_flags & ~OPT_OUT_BIT) { + /* RFC5155 3.1.2 */ + return false; + } + + return nsec3_flags & OPT_OUT_BIT; +} + +/** + * Checks whether NSEC3 RR matches the supplied name. + * @param flags Flags to be set according to check outcome. + * @param nsec3 NSEC3 RR. + * @param name Name to be checked. + * @return 0 if matching, >0 if not (abs(ENOENT)), or error code (<0). + */ +static int matches_name(const knot_rrset_t *nsec3, const knot_dname_t *name) +{ + if (kr_fails_assert(nsec3 && name)) + return kr_error(EINVAL); + + uint8_t hash_data[MAX_HASH_BYTES] = { 0, }; + dnssec_binary_t owner_hash = { 0, hash_data }; + dnssec_nsec3_params_t params = { 0, }; + dnssec_binary_t name_hash = { 0, }; + + int ret = read_owner_hash(&owner_hash, MAX_HASH_BYTES, nsec3); + if (ret != 0) + goto fail; + + ret = nsec3_parameters(¶ms, nsec3); + if (ret != 0) + goto fail; + + ret = hash_name(&name_hash, ¶ms, name); + if (ret != 0) + goto fail; + + if ((owner_hash.size == name_hash.size) && + (memcmp(owner_hash.data, name_hash.data, owner_hash.size) == 0)) { + ret = kr_ok(); + } else { + ret = abs(ENOENT); + } + +fail: + if (params.salt.data) + dnssec_nsec3_params_free(¶ms); + if (name_hash.data) + dnssec_binary_free(&name_hash); + return ret; +} +#undef MAX_HASH_BYTES + +/** + * Prepends an asterisk label to given name. + * + * @param tgt Target buffer to write domain name into. + * @param name Name to be added to the asterisk. + * @return Size of the resulting name or error code. + */ +static int prepend_asterisk(uint8_t *tgt, size_t maxlen, const knot_dname_t *name) +{ + if (kr_fails_assert(maxlen >= 3)) + return kr_error(EINVAL); + memcpy(tgt, "\1*", 3); + return knot_dname_to_wire(tgt + 2, name, maxlen - 2); +} + +/** + * Closest encloser proof (RFC5155 7.2.1). + * @note No RRSIGs are validated. + * @param pkt Packet structure to be processed. + * @param section_id Packet section to be processed. + * @param sname Name to be checked. + * @param encloser_name Returned matching encloser name, if found. + * @param matching_encloser_nsec3 Pointer to matching encloser NSEC RRSet. + * @param covering_next_nsec3 Pointer to covering next closer NSEC3 RRSet. + * @return 0 or error code. + */ +static int closest_encloser_proof(const knot_pkt_t *pkt, + knot_section_t section_id, + const knot_dname_t *sname, + const knot_dname_t **encloser_name, + const knot_rrset_t **matching_encloser_nsec3, + const knot_rrset_t **covering_next_nsec3) +{ + const knot_pktsection_t *sec = knot_pkt_section(pkt, section_id); + if (!sec || !sname) + return kr_error(EINVAL); + + const knot_rrset_t *matching = NULL; + const knot_rrset_t *covering = NULL; + + int flags = 0; + const knot_dname_t *next_closer = NULL; + for (unsigned i = 0; i < sec->count; ++i) { + const knot_rrset_t *rrset = knot_pkt_rr(sec, i); + if (rrset->type != KNOT_RRTYPE_NSEC3) + continue; + /* Also skip the NSEC3-to-match an ancestor of sname if it's + * a parent-side delegation, as that would mean the owner + * does not really exist (authoritatively in this zone, + * even in case of opt-out). + */ + const uint8_t *bm = knot_nsec3_bitmap(rrset->rrs.rdata); + uint16_t bm_size = knot_nsec3_bitmap_len(rrset->rrs.rdata); + if (kr_nsec_children_in_zone_check(bm, bm_size) != 0) + continue; /* no fatal errors from bad RRs */ + /* Match the NSEC3 to sname or one of its ancestors. */ + unsigned skipped = 0; + flags = 0; + int ret = closest_encloser_match(&flags, rrset, sname, &skipped); + if (ret != 0) + return ret; + if (!(flags & FLG_CLOSEST_PROVABLE_ENCLOSER)) + continue; + matching = rrset; + /* Construct the next closer name and try to cover it. */ + --skipped; + next_closer = sname; + for (unsigned j = 0; j < skipped; ++j) { + if (kr_fails_assert(next_closer[0])) + return kr_error(EINVAL); + next_closer = knot_wire_next_label(next_closer, NULL); + } + for (unsigned j = 0; j < sec->count; ++j) { + const knot_rrset_t *rrset_j = knot_pkt_rr(sec, j); + if (rrset_j->type != KNOT_RRTYPE_NSEC3) + continue; + ret = covers_name(&flags, rrset_j, next_closer); + if (ret != 0) + return ret; + if (flags & FLG_NAME_COVERED) { + covering = rrset_j; + break; + } + } + if (flags & FLG_NAME_COVERED) + break; + flags = 0; // + } + + if ((flags & FLG_CLOSEST_PROVABLE_ENCLOSER) && (flags & FLG_NAME_COVERED) && next_closer) { + if (encloser_name && next_closer[0]) + *encloser_name = knot_wire_next_label(next_closer, NULL); + if (matching_encloser_nsec3) + *matching_encloser_nsec3 = matching; + if (covering_next_nsec3) + *covering_next_nsec3 = covering; + return kr_ok(); + } + + return kr_error(ENOENT); +} + +/** + * Check whether any NSEC3 RR covers a wildcard RR at the closer encloser. + * @param pkt Packet structure to be processed. + * @param section_id Packet section to be processed. + * @param encloser Closest (provable) encloser domain name. + * @return 0 or error code: + * KNOT_ERANGE - NSEC3 RR (that covers a wildcard) + * has been found, but has opt-out flag set; + * otherwise - error. + */ +static int covers_closest_encloser_wildcard(const knot_pkt_t *pkt, knot_section_t section_id, + const knot_dname_t *encloser) +{ + const knot_pktsection_t *sec = knot_pkt_section(pkt, section_id); + if (!sec || !encloser) + return kr_error(EINVAL); + + uint8_t wildcard[KNOT_DNAME_MAXLEN]; + wildcard[0] = 1; + wildcard[1] = '*'; + int encloser_len = knot_dname_size(encloser); + if (encloser_len < 0) + return encloser_len; + memcpy(wildcard + 2, encloser, encloser_len); + + int flags = 0; + for (unsigned i = 0; i < sec->count; ++i) { + const knot_rrset_t *rrset = knot_pkt_rr(sec, i); + if (rrset->type != KNOT_RRTYPE_NSEC3) + continue; + int ret = covers_name(&flags, rrset, wildcard); + if (ret != 0) + return ret; + if (flags & FLG_NAME_COVERED) { + return has_optout(rrset) ? + kr_error(KNOT_ERANGE) : kr_ok(); + } + } + + return kr_error(ENOENT); +} + +int kr_nsec3_name_error_response_check(const knot_pkt_t *pkt, knot_section_t section_id, + const knot_dname_t *sname) +{ + const knot_dname_t *encloser = NULL; + const knot_rrset_t *covering_next_nsec3 = NULL; + int ret = closest_encloser_proof(pkt, section_id, sname, + &encloser, NULL, &covering_next_nsec3); + if (ret != 0) + return ret; + ret = covers_closest_encloser_wildcard(pkt, section_id, encloser); + if (ret != 0) { + /* OK, but NSEC3 for wildcard at encloser has opt-out; + * or error */ + return ret; + } + /* Closest encloser proof is OK and + * NSEC3 for wildcard has been found and optout flag is not set. + * Now check if NSEC3 that covers next closer name has opt-out. */ + return has_optout(covering_next_nsec3) ? + kr_error(KNOT_ERANGE) : kr_ok(); +} + +/** + * Search the packet section for a matching NSEC3 with nodata-proving bitmap. + * @param pkt Packet structure to be processed. + * @param section_id Packet section to be processed. + * @param sname Name to be checked. + * @param stype Type to be checked. + * @return 0 or error code. + * @note This does NOT check the opt-out case if type is DS; + * see RFC 5155 8.6. + */ +static int nodata_find(const knot_pkt_t *pkt, knot_section_t section_id, + const knot_dname_t *name, const uint16_t type) +{ + const knot_pktsection_t *sec = knot_pkt_section(pkt, section_id); + if (!sec || !name) + return kr_error(EINVAL); + + for (unsigned i = 0; i < sec->count; ++i) { + const knot_rrset_t *nsec3 = knot_pkt_rr(sec, i); + /* Records causing any errors are simply skipped. */ + if (nsec3->type != KNOT_RRTYPE_NSEC3 + || matches_name(nsec3, name) != kr_ok()) { + continue; + /* LATER(optim.): we repeatedly recompute the hash of `name` */ + } + + const uint8_t *bm = knot_nsec3_bitmap(nsec3->rrs.rdata); + uint16_t bm_size = knot_nsec3_bitmap_len(nsec3->rrs.rdata); + if (kr_nsec_bitmap_nodata_check(bm, bm_size, type, nsec3->owner) == kr_ok()) + return kr_ok(); + } + + return kr_error(ENOENT); +} + +/** + * Check whether NSEC3 RR matches a wildcard at the closest encloser and has given type bit missing. + * @param pkt Packet structure to be processed. + * @param section_id Packet section to be processed. + * @param encloser Closest (provable) encloser domain name. + * @param stype Type to be checked. + * @return 0 or error code. + */ +static int matches_closest_encloser_wildcard(const knot_pkt_t *pkt, knot_section_t section_id, + const knot_dname_t *encloser, uint16_t stype) +{ + const knot_pktsection_t *sec = knot_pkt_section(pkt, section_id); + if (!sec || !encloser) + return kr_error(EINVAL); + + uint8_t wildcard[KNOT_DNAME_MAXLEN]; /**< the source of synthesis */ + int ret = prepend_asterisk(wildcard, sizeof(wildcard), encloser); + if (ret < 0) + return ret; + kr_require(ret >= 3); + return nodata_find(pkt, section_id, wildcard, stype); +} + +int kr_nsec3_wildcard_answer_response_check(const knot_pkt_t *pkt, knot_section_t section_id, + const knot_dname_t *sname, int trim_to_next) +{ + const knot_pktsection_t *sec = knot_pkt_section(pkt, section_id); + if (!sec || !sname) + return kr_error(EINVAL); + + /* Compute the next closer name. */ + for (int i = 0; i < trim_to_next; ++i) { + if (kr_fails_assert(sname[0])) + return kr_error(EINVAL); + sname = knot_wire_next_label(sname, NULL); + } + + int flags = 0; + for (unsigned i = 0; i < sec->count; ++i) { + const knot_rrset_t *rrset = knot_pkt_rr(sec, i); + if (rrset->type != KNOT_RRTYPE_NSEC3) + continue; + if (kr_nsec3_limited_rdata(rrset->rrs.rdata)) { + /* Avoid hashing with too many iterations. + * If we get here, the `sname` wildcard probably ends up bogus, + * but it gets downgraded to KR_RANK_INSECURE when validator + * gets to verifying one of these over-limit NSEC3 RRs. */ + continue; + } + int ret = covers_name(&flags, rrset, sname); + if (ret != 0) + return ret; + if (flags & FLG_NAME_COVERED) { + return has_optout(rrset) ? + kr_error(KNOT_ERANGE) : kr_ok(); + } + } + + return kr_error(ENOENT); +} + + +int kr_nsec3_no_data(const knot_pkt_t *pkt, knot_section_t section_id, + const knot_dname_t *sname, uint16_t stype) +{ + /* DS record may be also matched by an existing NSEC3 RR. */ + int ret = nodata_find(pkt, section_id, sname, stype); + if (ret == 0) { + /* Satisfies RFC5155 8.5 and 8.6, both first paragraph. */ + return ret; + } + + /* Find closest provable encloser. */ + const knot_dname_t *encloser_name = NULL; + const knot_rrset_t *covering_next_nsec3 = NULL; + ret = closest_encloser_proof(pkt, section_id, sname, &encloser_name, + NULL, &covering_next_nsec3); + if (ret != 0) + return ret; + + if (kr_fails_assert(encloser_name && covering_next_nsec3)) + return kr_error(EFAULT); + ret = matches_closest_encloser_wildcard(pkt, section_id, + encloser_name, stype); + if (ret == 0) { + /* Satisfies RFC5155 8.7 */ + if (has_optout(covering_next_nsec3)) { + /* Opt-out is detected. + * Despite the fact that all records + * in the packet can be properly signed, + * AD bit must not be set due to rfc5155 9.2. + * Return appropriate code to the caller */ + ret = kr_error(KNOT_ERANGE); + } + return ret; + } + + if (!has_optout(covering_next_nsec3)) { + /* Bogus */ + ret = kr_error(ENOENT); + } else { + /* + * Satisfies RFC5155 8.6 (QTYPE == DS), 2nd paragraph. + * Also satisfies ERRATA 3441 8.5 (QTYPE != DS), 3rd paragraph. + * - (wildcard) empty nonterminal + * derived from insecure delegation. + * Denial of existence can not be proven. + * Set error code to proceed insecure. + */ + ret = kr_error(KNOT_ERANGE); + } + + return ret; +} + +int kr_nsec3_ref_to_unsigned(const knot_pkt_t *pkt) +{ + const knot_pktsection_t *sec = knot_pkt_section(pkt, KNOT_AUTHORITY); + if (!sec) + return kr_error(EINVAL); + for (unsigned i = 0; i < sec->count; ++i) { + const knot_rrset_t *ns = knot_pkt_rr(sec, i); + if (ns->type == KNOT_RRTYPE_DS) + return kr_error(EEXIST); + if (ns->type != KNOT_RRTYPE_NS) + continue; + + bool nsec3_found = false; + for (unsigned j = 0; j < sec->count; ++j) { + const knot_rrset_t *nsec3 = knot_pkt_rr(sec, j); + if (nsec3->type == KNOT_RRTYPE_DS) + return kr_error(EEXIST); + if (nsec3->type != KNOT_RRTYPE_NSEC3) + continue; + nsec3_found = true; + /* nsec3 found, check if owner name matches the delegation name. + * Just skip in case of *any* errors. */ + if (matches_name(nsec3, ns->owner) != kr_ok()) + continue; + + const uint8_t *bm = knot_nsec3_bitmap(nsec3->rrs.rdata); + uint16_t bm_size = knot_nsec3_bitmap_len(nsec3->rrs.rdata); + if (!bm) + return kr_error(EINVAL); + if (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)) { + /* Satisfies rfc5155, 8.9. paragraph 2 */ + return kr_ok(); + } + } + if (!nsec3_found) + return kr_error(DNSSEC_NOT_FOUND); + /* nsec3 that matches the delegation was not found. + * Check rfc5155, 8.9. paragraph 4. + * Find closest provable encloser. + */ + const knot_dname_t *encloser_name = NULL; + const knot_rrset_t *covering_next_nsec3 = NULL; + int ret = closest_encloser_proof(pkt, KNOT_AUTHORITY, ns->owner, + &encloser_name, NULL, &covering_next_nsec3); + if (ret != 0) + return kr_error(EINVAL); + + if (has_optout(covering_next_nsec3)) { + return kr_error(KNOT_ERANGE); + } else { + return kr_error(EINVAL); + } + } + return kr_error(EINVAL); +} + +int kr_nsec3_matches_name_and_type(const knot_rrset_t *nsec3, + 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 don't (currently) only use this API for NS. See RFC 6840 sec. 4. + */ + if (kr_fails_assert(type == KNOT_RRTYPE_NS)) + return kr_error(EINVAL); + int ret = matches_name(nsec3, name); + if (ret) + return kr_error(ret); + const uint8_t *bm = knot_nsec3_bitmap(nsec3->rrs.rdata); + uint16_t bm_size = knot_nsec3_bitmap_len(nsec3->rrs.rdata); + if (!bm) + return kr_error(EINVAL); + if (dnssec_nsec_bitmap_contains(bm, bm_size, type)) { + return kr_ok(); + } else { + return kr_error(ENOENT); + } +} diff --git a/lib/dnssec/nsec3.h b/lib/dnssec/nsec3.h new file mode 100644 index 0000000..a28d3c7 --- /dev/null +++ b/lib/dnssec/nsec3.h @@ -0,0 +1,116 @@ +/* Copyright (C) CZ.NIC, z.s.p.o. <knot-resolver@labs.nic.cz> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include <libknot/packet/pkt.h> +#include <libknot/rrtype/nsec3.h> +#include <libdnssec/nsec.h> + + +static inline unsigned int kr_nsec3_price(unsigned int iterations, unsigned int salt_len) +{ + // SHA1 works on 64-byte chunks. + // On iterating we hash the salt + 20 bytes of the previous hash. + int chunks_per_iter = (20 + salt_len - 1) / 64 + 1; + return (iterations + 1) * chunks_per_iter; +} + +/** High numbers in NSEC3 iterations don't really help security + * + * ...so we avoid doing all the work. The limit is a current compromise; + * answers using NSEC3 over kr_nsec3_limited* get downgraded to insecure status. + * + https://datatracker.ietf.org/doc/html/rfc9276#name-recommendation-for-validati + */ +static inline bool kr_nsec3_limited(unsigned int iterations, unsigned int salt_len) +{ + const int MAX_ITERATIONS = 50; // limit with short salt length + return kr_nsec3_price(iterations, salt_len) > MAX_ITERATIONS + 1; +} +static inline bool kr_nsec3_limited_rdata(const knot_rdata_t *rd) +{ + return kr_nsec3_limited(knot_nsec3_iters(rd), knot_nsec3_salt_len(rd)); +} +static inline bool kr_nsec3_limited_params(const dnssec_nsec3_params_t *params) +{ + return kr_nsec3_limited(params->iterations, params->salt.size); +} + +/** Return limit on NSEC3 depth. The point is to avoid doing too much work on SHA1. + * + * CVE-2023-50868: NSEC3 closest encloser proof can exhaust CPU + * + * 128 is chosen so that zones with good NSEC3 parameters (giving _price() == 1) + * won't be limited in any way. Performance doesn't seem too bad with that either. + */ +static inline int kr_nsec3_max_depth(const dnssec_nsec3_params_t *params) +{ + return 128 / kr_nsec3_price(params->iterations, params->salt.size); +} + + +/** + * Name error response check (RFC5155 7.2.2). + * @note No RRSIGs are validated. + * @param pkt Packet structure to be processed. + * @param section_id Packet section to be processed. + * @param sname Name to be checked. + * @return 0 or error code. + */ +int kr_nsec3_name_error_response_check(const knot_pkt_t *pkt, knot_section_t section_id, + const knot_dname_t *sname); + +/** + * Wildcard answer response check (RFC5155 7.2.6). + * @param pkt Packet structure to be processed. + * @param section_id Packet section to be processed. + * @param sname Name to be checked. + * @param trim_to_next Number of labels to remove to obtain next closer name. + * @return 0 or error code: + * KNOT_ERANGE - NSEC3 RR that covers a wildcard + * has been found, but has opt-out flag set; + * otherwise - error. + * Too expensive NSEC3 records are skipped, so you probably get kr_error(ENOENT). + */ +int kr_nsec3_wildcard_answer_response_check(const knot_pkt_t *pkt, knot_section_t section_id, + const knot_dname_t *sname, int trim_to_next); + +/** + * Authenticated denial of existence according to RFC5155 8.5 and 8.7. + * @note No RRSIGs are validated. + * @param pkt Packet structure to be processed. + * @param section_id Packet section to be processed. + * @param sname Queried domain name. + * @param stype Queried type. + * @return 0 or error code: + * DNSSEC_NOT_FOUND - neither ds nor nsec records + * were not found. + * KNOT_ERANGE - denial of existence can't be proven + * due to opt-out, otherwise - bogus. + */ +int kr_nsec3_no_data(const knot_pkt_t *pkt, knot_section_t section_id, + const knot_dname_t *sname, uint16_t stype); + +/** + * Referral to unsigned subzone check (RFC5155 8.9). + * @note No RRSIGs are validated. + * @param pkt Packet structure to be processed. + * @return 0 or error code: + * KNOT_ERANGE - denial of existence can't be proven + * due to opt-out. + * EEXIST - ds record was found. + * EINVAL - bogus. + */ +int kr_nsec3_ref_to_unsigned(const knot_pkt_t *pkt); + +/** + * Checks whether supplied NSEC3 RR matches the supplied name and NS type. + * @param nsec3 NSEC3 RR. + * @param name Name to be checked. + * @param type Type to be checked. Only use with NS! TODO + * @return 0 or error code. + */ +int kr_nsec3_matches_name_and_type(const knot_rrset_t *nsec3, + const knot_dname_t *name, uint16_t type); diff --git a/lib/dnssec/signature.c b/lib/dnssec/signature.c new file mode 100644 index 0000000..aadb5cb --- /dev/null +++ b/lib/dnssec/signature.c @@ -0,0 +1,304 @@ +/* Copyright (C) CZ.NIC, z.s.p.o. <knot-resolver@labs.nic.cz> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include <arpa/inet.h> +#include <string.h> + +#include <libdnssec/error.h> +#include <libdnssec/key.h> +#include <libdnssec/sign.h> +#include <libknot/descriptor.h> +#include <libknot/packet/rrset-wire.h> +#include <libknot/packet/wire.h> +#include <libknot/rrset.h> +#include <libknot/rrtype/rrsig.h> +#include <libknot/rrtype/ds.h> +#include <libknot/wire.h> + +#include "lib/defines.h" +#include "lib/utils.h" +#include "lib/dnssec/signature.h" + +static int authenticate_ds(const dnssec_key_t *key, dnssec_binary_t *ds_rdata, uint8_t digest_type) +{ + /* Compute DS RDATA from the DNSKEY. */ + dnssec_binary_t computed_ds = { 0, }; + int ret = dnssec_key_create_ds(key, digest_type, &computed_ds); + if (ret != DNSSEC_EOK) + goto fail; + + /* DS records contain algorithm, key tag and the digest. + * Therefore the comparison of the two DS is sufficient. + */ + ret = (ds_rdata->size == computed_ds.size) && + (memcmp(ds_rdata->data, computed_ds.data, ds_rdata->size) == 0); + ret = ret ? kr_ok() : kr_error(ENOENT); + +fail: + dnssec_binary_free(&computed_ds); + return kr_error(ret); +} + +int kr_authenticate_referral(const knot_rrset_t *ref, const dnssec_key_t *key) +{ + if (kr_fails_assert(ref && key)) + return kr_error(EINVAL); + if (ref->type != KNOT_RRTYPE_DS) + return kr_error(EINVAL); + + /* Determine whether to ignore SHA1 digests, because: + https://datatracker.ietf.org/doc/html/rfc4509#section-3 + * Now, the RFCs seem to only mention SHA1 and SHA256 (e.g. no SHA384), + * but the most natural extension is to make any other algorithm trump SHA1. + * (Note that the old GOST version is already unsupported by libdnssec.) */ + bool skip_sha1 = false; + knot_rdata_t *rd = ref->rrs.rdata; + for (int i = 0; i < ref->rrs.count; ++i, rd = knot_rdataset_next(rd)) { + const uint8_t algo = knot_ds_digest_type(rd); + if (algo != DNSSEC_KEY_DIGEST_SHA1 && dnssec_algorithm_digest_support(algo)) { + skip_sha1 = true; + break; + } + } + /* But otherwise try all possible DS records. */ + int ret = 0; + rd = ref->rrs.rdata; + for (int i = 0; i < ref->rrs.count; ++i, rd = knot_rdataset_next(rd)) { + const uint8_t algo = knot_ds_digest_type(rd); + if (skip_sha1 && algo == DNSSEC_KEY_DIGEST_SHA1) + continue; + dnssec_binary_t ds_rdata = { + .size = rd->len, + .data = rd->data + }; + ret = authenticate_ds(key, &ds_rdata, algo); + if (ret == 0) /* Found a good DS */ + return kr_ok(); + } + + return kr_error(ret); +} + +/** + * Adjust TTL in wire format. + * @param wire RR Set in wire format. + * @param wire_size Size of the wire data portion. + * @param new_ttl TTL value to be set for all RRs. + * @return 0 or error code. + */ +static int adjust_wire_ttl(uint8_t *wire, size_t wire_size, uint32_t new_ttl) +{ + if (kr_fails_assert(wire)) + return kr_error(EINVAL); + static_assert(sizeof(uint16_t) == 2, "uint16_t must be exactly 2 bytes"); + static_assert(sizeof(uint32_t) == 4, "uint32_t must be exactly 4 bytes"); + uint16_t rdlen; + + int ret; + + new_ttl = htonl(new_ttl); + + size_t i = 0; + /* RR wire format in RFC1035 3.2.1 */ + while(i < wire_size) { + ret = knot_dname_size(wire + i); + if (ret < 0) + return ret; + i += ret + 4; + memcpy(wire + i, &new_ttl, sizeof(uint32_t)); + i += sizeof(uint32_t); + + memcpy(&rdlen, wire + i, sizeof(uint16_t)); + rdlen = ntohs(rdlen); + i += sizeof(uint16_t) + rdlen; + + if (kr_fails_assert(i <= wire_size)) + return kr_error(EINVAL); + } + + return kr_ok(); +} + +/*! + * \brief Add RRSIG RDATA without signature to signing context. + * + * Requires signer name in RDATA in canonical form. + * + * \param ctx Signing context. + * \param rdata Pointer to RRSIG RDATA. + * + * \return Error code, KNOT_EOK if successful. + */ +#define RRSIG_RDATA_SIGNER_OFFSET 18 +static int sign_ctx_add_self(dnssec_sign_ctx_t *ctx, const uint8_t *rdata) +{ + if (kr_fails_assert(ctx && rdata)) + return kr_error(EINVAL); + + int result; + + // static header + + dnssec_binary_t header = { + .data = (uint8_t *)rdata, + .size = RRSIG_RDATA_SIGNER_OFFSET, + }; + + result = dnssec_sign_add(ctx, &header); + if (result != DNSSEC_EOK) + return result; + + // signer name + + const uint8_t *rdata_signer = rdata + RRSIG_RDATA_SIGNER_OFFSET; + dnssec_binary_t signer = { 0 }; + signer.data = knot_dname_copy(rdata_signer, NULL); + signer.size = knot_dname_size(signer.data); + + result = dnssec_sign_add(ctx, &signer); + free(signer.data); + + return result; +} +#undef RRSIG_RDATA_SIGNER_OFFSET + +/*! + * \brief Add covered RRs to signing context. + * + * Requires all DNAMEs in canonical form and all RRs ordered canonically. + * + * \param ctx Signing context. + * \param covered Covered RRs. + * + * \return Error code, KNOT_EOK if successful. + */ +static int sign_ctx_add_records(dnssec_sign_ctx_t *ctx, const knot_rrset_t *covered, + uint32_t orig_ttl, int trim_labels) +{ + if (!ctx || !covered || trim_labels < 0) + return kr_error(EINVAL); + + // huge block of rrsets can be optionally created + static uint8_t wire_buffer[KNOT_WIRE_MAX_PKTSIZE]; + int written = knot_rrset_to_wire(covered, wire_buffer, sizeof(wire_buffer), NULL); + if (written < 0) + return written; + + /* Set original ttl. */ + int ret = adjust_wire_ttl(wire_buffer, written, orig_ttl); + if (ret != 0) + return ret; + + if (!trim_labels) { + const dnssec_binary_t wire_binary = { + .size = written, + .data = wire_buffer + }; + return dnssec_sign_add(ctx, &wire_binary); + } + + /* RFC4035 5.3.2 + * Remove leftmost labels and replace them with '*.' + * for each RR in covered. + */ + uint8_t *beginp = wire_buffer; + for (uint16_t i = 0; i < covered->rrs.count; ++i) { + /* RR(i) = name | type | class | OrigTTL | RDATA length | RDATA */ + for (int j = 0; j < trim_labels; ++j) { + if (kr_fails_assert(beginp[0])) + return kr_error(EINVAL); + beginp = (uint8_t *) knot_wire_next_label(beginp, NULL); + if (kr_fails_assert(beginp)) + return kr_error(EFAULT); + } + *(--beginp) = '*'; + *(--beginp) = 1; + const size_t rdatalen_offset = knot_dname_size(beginp) + /* name */ + sizeof(uint16_t) + /* type */ + sizeof(uint16_t) + /* class */ + sizeof(uint32_t); /* OrigTTL */ + const uint8_t *rdatalen_ptr = beginp + rdatalen_offset; + const uint16_t rdata_size = knot_wire_read_u16(rdatalen_ptr); + const size_t rr_size = rdatalen_offset + + sizeof(uint16_t) + /* RDATA length */ + rdata_size; /* RDATA */ + const dnssec_binary_t wire_binary = { + .size = rr_size, + .data = beginp + }; + ret = dnssec_sign_add(ctx, &wire_binary); + if (ret != 0) + break; + beginp += rr_size; + } + return ret; +} + +/*! + * \brief Add all data covered by signature into signing context. + * + * RFC 4034: The signature covers RRSIG RDATA field (excluding the signature) + * and all matching RR records, which are ordered canonically. + * + * Requires all DNAMEs in canonical form and all RRs ordered canonically. + * + * \param ctx Signing context. + * \param rrsig_rdata RRSIG RDATA with populated fields except signature. + * \param covered Covered RRs. + * + * \return Error code, KNOT_EOK if successful. + */ +/* TODO -- Taken from knot/src/knot/dnssec/rrset-sign.c. Re-write for better fit needed. */ +static int sign_ctx_add_data(dnssec_sign_ctx_t *ctx, const uint8_t *rrsig_rdata, + const knot_rrset_t *covered, uint32_t orig_ttl, int trim_labels) +{ + int result = sign_ctx_add_self(ctx, rrsig_rdata); + if (result != KNOT_EOK) + return result; + + return sign_ctx_add_records(ctx, covered, orig_ttl, trim_labels); +} + +int kr_check_signature(const knot_rdata_t *rrsig, + const dnssec_key_t *key, const knot_rrset_t *covered, + int trim_labels) +{ + if (!rrsig || !key || !dnssec_key_can_verify(key)) + return kr_error(EINVAL); + + int ret = 0; + dnssec_sign_ctx_t *sign_ctx = NULL; + dnssec_binary_t signature = { + .data = /*const-cast*/(uint8_t*)knot_rrsig_signature(rrsig), + .size = knot_rrsig_signature_len(rrsig), + }; + if (!signature.data || !signature.size) { + ret = kr_error(EINVAL); + goto fail; + } + + if (dnssec_sign_new(&sign_ctx, key) != 0) { + ret = kr_error(ENOMEM); + goto fail; + } + + uint32_t orig_ttl = knot_rrsig_original_ttl(rrsig); + + if (sign_ctx_add_data(sign_ctx, rrsig->data, covered, orig_ttl, trim_labels) != 0) { + ret = kr_error(ENOMEM); + goto fail; + } + + ret = dnssec_sign_verify(sign_ctx, false, &signature); + if (ret != 0) { + ret = kr_error(EBADMSG); + goto fail; + } + + ret = kr_ok(); + +fail: + dnssec_sign_free(sign_ctx); + return ret; +} diff --git a/lib/dnssec/signature.h b/lib/dnssec/signature.h new file mode 100644 index 0000000..1cc6c8f --- /dev/null +++ b/lib/dnssec/signature.h @@ -0,0 +1,29 @@ +/* Copyright (C) CZ.NIC, z.s.p.o. <knot-resolver@labs.nic.cz> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include <libdnssec/key.h> +#include <libknot/rrset.h> + +/** + * Performs referral authentication according to RFC4035 5.2, bullet 2 + * @param ref Referral RRSet. Currently only DS can be used. + * @param key Already parsed key. + * @return 0 or error code. In particular: DNSSEC_INVALID_DS_ALGORITHM + * in case *all* DSs in ref use an unimplemented algorithm. + */ +int kr_authenticate_referral(const knot_rrset_t *ref, const dnssec_key_t *key); + +/** + * Check the signature of the supplied RRSet. + * @param rrsig A single signature. + * @param key Key to be used to validate the signature. + * @param covered The covered RRSet. + * @param trim_labels Number of the leftmost labels to be removed and replaced with '*.'. + * @return 0 if signature valid, error code else. + */ +int kr_check_signature(const knot_rdata_t *rrsig, + const dnssec_key_t *key, const knot_rrset_t *covered, + int trim_labels); diff --git a/lib/dnssec/ta.c b/lib/dnssec/ta.c new file mode 100644 index 0000000..becf7d8 --- /dev/null +++ b/lib/dnssec/ta.c @@ -0,0 +1,154 @@ +/* Copyright (C) CZ.NIC, z.s.p.o. <knot-resolver@labs.nic.cz> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include <contrib/cleanup.h> +#include <libknot/descriptor.h> +#include <libknot/rdataset.h> +#include <libknot/rrset.h> +#include <libknot/packet/wire.h> +#include <libdnssec/key.h> +#include <libdnssec/error.h> + +#include "lib/defines.h" +#include "lib/dnssec.h" +#include "lib/dnssec/ta.h" +#include "lib/resolve.h" +#include "lib/utils.h" + +knot_rrset_t *kr_ta_get(trie_t *trust_anchors, const knot_dname_t *name) +{ + trie_val_t *val = trie_get_try(trust_anchors, (const char *)name, strlen((const char *)name)); + return (val) ? *val : NULL; +} + +const knot_dname_t * kr_ta_closest(const struct kr_context *ctx, const knot_dname_t *name, + const uint16_t type) +{ + kr_require(ctx && name); + if (type == KNOT_RRTYPE_DS && name[0] != '\0') { + /* DS is parent-side record, so the parent name needs to be covered. */ + name = knot_wire_next_label(name, NULL); + } + while (name) { + struct kr_context *ctx_nc = (struct kr_context *)/*const-cast*/ctx; + if (kr_ta_get(ctx_nc->trust_anchors, name)) { + return name; + } + if (kr_ta_get(ctx_nc->negative_anchors, name)) { + return NULL; + } + name = knot_wire_next_label(name, NULL); + } + return NULL; +} + +/* @internal Create DS from DNSKEY, caller MUST free dst if successful. */ +static int dnskey2ds(dnssec_binary_t *dst, const knot_dname_t *owner, const uint8_t *rdata, uint16_t rdlen) +{ + dnssec_key_t *key = NULL; + int ret = dnssec_key_new(&key); + if (ret) goto cleanup; + /* Create DS from DNSKEY and reinsert */ + const dnssec_binary_t key_data = { .size = rdlen, .data = (uint8_t *)rdata }; + ret = dnssec_key_set_rdata(key, &key_data); + if (ret) goto cleanup; + /* Accept only keys with Zone and SEP flags that aren't revoked, + * as a precaution. RFC 5011 also utilizes these flags. + * TODO: kr_dnssec_key_* names are confusing. */ + const bool flags_ok = kr_dnssec_key_zsk(rdata) && !kr_dnssec_key_revoked(rdata); + if (!flags_ok) { + auto_free char *owner_str = kr_dname_text(owner); + kr_log_error(TA, "refusing to trust %s DNSKEY because of flags %d\n", + owner_str, dnssec_key_get_flags(key)); + ret = kr_error(EILSEQ); + goto cleanup; + } else if (!kr_dnssec_key_ksk(rdata)) { + auto_free char *owner_str = kr_dname_text(owner); + int flags = dnssec_key_get_flags(key); + kr_log_warning(TA, "warning: %s DNSKEY is missing the SEP bit; " + "flags %d instead of %d\n", + owner_str, flags, flags + 1/*a little ugly*/); + } + ret = dnssec_key_set_dname(key, owner); + if (ret) goto cleanup; + ret = dnssec_key_create_ds(key, DNSSEC_KEY_DIGEST_SHA256, dst); +cleanup: + dnssec_key_free(key); + return kr_error(ret); +} + +/* @internal Insert new TA to trust anchor set, rdata MUST be of DS type. */ +static int insert_ta(trie_t *trust_anchors, const knot_dname_t *name, + uint32_t ttl, const uint8_t *rdata, uint16_t rdlen) +{ + bool is_new_key = false; + knot_rrset_t *ta_rr = kr_ta_get(trust_anchors, name); + if (!ta_rr) { + ta_rr = knot_rrset_new(name, KNOT_RRTYPE_DS, KNOT_CLASS_IN, ttl, NULL); + is_new_key = true; + } + /* Merge-in new key data */ + if (!ta_rr || (rdlen > 0 && knot_rrset_add_rdata(ta_rr, rdata, rdlen, NULL) != 0)) { + knot_rrset_free(ta_rr, NULL); + return kr_error(ENOMEM); + } + if (is_new_key) { + trie_val_t *val = trie_get_ins(trust_anchors, (const char *)name, strlen((const char *)name)); + if (kr_fails_assert(val)) + return kr_error(EINVAL); + *val = ta_rr; + } + return kr_ok(); +} + +int kr_ta_add(trie_t *trust_anchors, const knot_dname_t *name, uint16_t type, + uint32_t ttl, const uint8_t *rdata, uint16_t rdlen) +{ + if (!trust_anchors || !name) { + return kr_error(EINVAL); + } + + /* DS/DNSKEY types are accepted, for DNSKEY we + * need to compute a DS digest. */ + if (type == KNOT_RRTYPE_DS) { + return insert_ta(trust_anchors, name, ttl, rdata, rdlen); + } else if (type == KNOT_RRTYPE_DNSKEY) { + dnssec_binary_t ds_rdata = { 0, }; + int ret = dnskey2ds(&ds_rdata, name, rdata, rdlen); + if (ret != 0) { + return ret; + } + ret = insert_ta(trust_anchors, name, ttl, ds_rdata.data, ds_rdata.size); + dnssec_binary_free(&ds_rdata); + return ret; + } else { /* Invalid type for TA */ + return kr_error(EINVAL); + } +} + +/* Delete record data */ +static int del_record(trie_val_t *v, void *ext) +{ + knot_rrset_t *ta_rr = *v; + if (ta_rr) { + knot_rrset_free(ta_rr, NULL); + } + return 0; +} + +int kr_ta_del(trie_t *trust_anchors, const knot_dname_t *name) +{ + knot_rrset_t *ta_rr; + int ret = trie_del(trust_anchors, (const char *)name, strlen((const char *)name), + (trie_val_t *) &ta_rr); + if (ret == KNOT_EOK && ta_rr) + knot_rrset_free(ta_rr, NULL); + return kr_ok(); +} + +void kr_ta_clear(trie_t *trust_anchors) +{ + trie_apply(trust_anchors, del_record, NULL); + trie_clear(trust_anchors); +} diff --git a/lib/dnssec/ta.h b/lib/dnssec/ta.h new file mode 100644 index 0000000..1eb1dd9 --- /dev/null +++ b/lib/dnssec/ta.h @@ -0,0 +1,61 @@ +/* Copyright (C) CZ.NIC, z.s.p.o. <knot-resolver@labs.nic.cz> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "lib/defines.h" +#include "lib/generic/trie.h" +#include <libknot/rrset.h> + +/** + * Find TA RRSet by name. + * @param trust_anchors trust store + * @param name name of the TA + * @return non-empty RRSet or NULL + */ +KR_EXPORT +knot_rrset_t *kr_ta_get(trie_t *trust_anchors, const knot_dname_t *name); + +/** + * Add TA to trust store. DS or DNSKEY types are supported. + * @param trust_anchors trust store + * @param name name of the TA + * @param type RR type of the TA (DS or DNSKEY) + * @param ttl + * @param rdata + * @param rdlen + * @return 0 or an error + */ +KR_EXPORT +int kr_ta_add(trie_t *trust_anchors, const knot_dname_t *name, uint16_t type, + uint32_t ttl, const uint8_t *rdata, uint16_t rdlen); + +struct kr_context; + +/** + * Return pointer to the name of the closest positive trust anchor or NULL. + * + * "Closest" means on path towards root. Closer negative anchor results into NULL. + * @param type serves as a shorthand because DS needs to start one level higher. + */ +KR_EXPORT KR_PURE +const knot_dname_t * kr_ta_closest(const struct kr_context *ctx, const knot_dname_t *name, + const uint16_t type); + +/** + * Remove TA from trust store. + * @param trust_anchors trust store + * @param name name of the TA + * @return 0 or an error + */ +KR_EXPORT +int kr_ta_del(trie_t *trust_anchors, const knot_dname_t *name); + +/** + * Clear trust store. + * @param trust_anchors trust store + */ +KR_EXPORT +void kr_ta_clear(trie_t *trust_anchors); + |