diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 10:41:58 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 10:41:58 +0000 |
commit | 1852910ef0fd7393da62b88aee66ee092208748e (patch) | |
tree | ad3b659dbbe622b58a5bda4fe0b5e1d80eee9277 /lib/dnssec | |
parent | Initial commit. (diff) | |
download | knot-resolver-1852910ef0fd7393da62b88aee66ee092208748e.tar.xz knot-resolver-1852910ef0fd7393da62b88aee66ee092208748e.zip |
Adding upstream version 5.3.1.upstream/5.3.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'lib/dnssec')
-rw-r--r-- | lib/dnssec/nsec.c | 528 | ||||
-rw-r--r-- | lib/dnssec/nsec.h | 93 | ||||
-rw-r--r-- | lib/dnssec/nsec3.c | 768 | ||||
-rw-r--r-- | lib/dnssec/nsec3.h | 82 | ||||
-rw-r--r-- | lib/dnssec/signature.c | 300 | ||||
-rw-r--r-- | lib/dnssec/signature.h | 29 | ||||
-rw-r--r-- | lib/dnssec/ta.c | 173 | ||||
-rw-r--r-- | lib/dnssec/ta.h | 75 |
8 files changed, 2048 insertions, 0 deletions
diff --git a/lib/dnssec/nsec.c b/lib/dnssec/nsec.c new file mode 100644 index 0000000..caab245 --- /dev/null +++ b/lib/dnssec/nsec.c @@ -0,0 +1,528 @@ +/* Copyright (C) 2015-2017 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include <assert.h> +#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" + + +int kr_nsec_children_in_zone_check(const uint8_t *bm, uint16_t bm_size) +{ + if (!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. + */ +} + +/** + * Check whether the NSEC RR proves that there is no closer match for <SNAME, SCLASS>. + * @param nsec NSEC RRSet. + * @param sname Searched name. + * @return 0 if proves, >0 if not (abs(ENOENT)), or error code (<0). + */ +static int nsec_covers(const knot_rrset_t *nsec, const knot_dname_t *sname) +{ + assert(nsec && sname); + if (knot_dname_cmp(sname, nsec->owner) <= 0) { + return abs(ENOENT); /* 'sname' before 'owner', so can't be covered */ + } + + /* If NSEC 'owner' >= 'next', it means that there is nothing after 'owner' */ + /* We have to lower-case it 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 (ret < 0) { + assert(!ret); + return kr_error(ret); + } + knot_dname_to_lower(next); + + const bool is_last_nsec = knot_dname_cmp(nsec->owner, next) >= 0; + const bool in_range = is_last_nsec || knot_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); +} + +#define FLG_NOEXIST_RRTYPE (1 << 0) /**< <SNAME, SCLASS> exists, <SNAME, SCLASS, STYPE> does not exist. */ +#define FLG_NOEXIST_RRSET (1 << 1) /**< <SNAME, SCLASS> does not exist. */ +#define FLG_NOEXIST_WILDCARD (1 << 2) /**< No wildcard covering <SNAME, SCLASS> exists. */ +#define FLG_NOEXIST_CLOSER (1 << 3) /**< Wildcard covering <SNAME, SCLASS> exists, but doesn't match STYPE. */ + + +/** + * According to set flags determine whether NSEC proving + * RRset or RRType non-existense has been found. + * @param f Flags to inspect. + * @return True if required NSEC exists. + */ +#define kr_nsec_rrset_noexist(f) \ + ((f) & (FLG_NOEXIST_RRTYPE | FLG_NOEXIST_RRSET)) +/** + * According to set flags determine whether wildcard non-existense + * has been proven. + * @param f Flags to inspect. + * @return True if wildcard not exists. + */ +#define kr_nsec_wcard_noexist(f) ((f) & FLG_NOEXIST_WILDCARD) + +/** + * According to set flags determine whether authenticated denial of existence has been proven. + * @param f Flags to inspect. + * @return True if denial of existence proven. + */ +#define kr_nsec_existence_denied(f) \ + ((kr_nsec_rrset_noexist(f)) && (kr_nsec_wcard_noexist(f))) + +/** + * Name error response check (RFC4035 3.1.3.2; RFC4035 5.4, bullet 2). + * @note Returned flags must be checked in order to prove denial. + * @param flags Flags to be set according to check outcome. + * @param nsec NSEC RR. + * @param name Name to be checked. + * @param pool + * @return 0 or error code. + */ +static int name_error_response_check_rr(int *flags, const knot_rrset_t *nsec, + const knot_dname_t *name) +{ + assert(flags && nsec && name); + + if (nsec_covers(nsec, name) == 0) { + *flags |= FLG_NOEXIST_RRSET; + } + + /* Try to find parent wildcard that is proved by this NSEC. */ + uint8_t namebuf[KNOT_DNAME_MAXLEN]; + int ret = knot_dname_to_wire(namebuf, name, sizeof(namebuf)); + if (ret < 0) + return ret; + knot_dname_t *ptr = namebuf; + while (ptr[0]) { + /* Remove leftmost label and replace it with '\1*'. */ + ptr = (uint8_t *) knot_wire_next_label(ptr, NULL); + if (!ptr) { + return kr_error(EINVAL); + } + *(--ptr) = '*'; + *(--ptr) = 1; + /* True if this wildcard provably doesn't exist. */ + if (nsec_covers(nsec, ptr) == 0) { + *flags |= FLG_NOEXIST_WILDCARD; + break; + } + /* Remove added leftmost asterisk. */ + ptr += 2; + } + + return kr_ok(); +} + +int kr_nsec_name_error_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); + } + + 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_NSEC) { + continue; + } + int ret = name_error_response_check_rr(&flags, rrset, sname); + if (ret != 0) { + return ret; + } + } + + return kr_nsec_existence_denied(flags) ? kr_ok() : kr_error(ENOENT); +} + +/** + * Returns the labels from the covering RRSIG RRs. + * @note The number must be the same in all covering RRSIGs. + * @param nsec NSEC RR. + * @param sec Packet section. + * @param Number of labels or (negative) error code. + */ +static int coverign_rrsig_labels(const knot_rrset_t *nsec, const knot_pktsection_t *sec) +{ + assert(nsec && sec); + + int ret = kr_error(ENOENT); + + for (unsigned i = 0; i < sec->count; ++i) { + const knot_rrset_t *rrset = knot_pkt_rr(sec, i); + if ((rrset->type != KNOT_RRTYPE_RRSIG) || + (!knot_dname_is_equal(rrset->owner, nsec->owner))) { + continue; + } + + knot_rdata_t *rdata_j = rrset->rrs.rdata; + for (uint16_t j = 0; j < rrset->rrs.count; + ++j, rdata_j = knot_rdataset_next(rdata_j)) { + if (knot_rrsig_type_covered(rdata_j) != KNOT_RRTYPE_NSEC) { + continue; + } + + if (ret < 0) { + ret = knot_rrsig_labels(rdata_j); + } else { + if (ret != knot_rrsig_labels(rdata_j)) { + return kr_error(EINVAL); + } + } + } + } + + return ret; +} + + +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(); +} + +/** + * Attempt to prove NODATA given a matching NSEC. + * @param flags Flags to be set according to check outcome. + * @param nsec NSEC RR. + * @param type Type to be checked. + * @return 0 on success, abs(ENOENT) for no proof, or error code (<0). + * @note It's not a *full* proof, of course (wildcards, etc.) + * @TODO returning result via `flags` is just ugly. + */ +static int no_data_response_check_rrtype(int *flags, const knot_rrset_t *nsec, + uint16_t type) +{ + assert(flags && nsec); + + const uint8_t *bm = knot_nsec_bitmap(nsec->rrs.rdata); + uint16_t bm_size = knot_nsec_bitmap_len(nsec->rrs.rdata); + int ret = kr_nsec_bitmap_nodata_check(bm, bm_size, type, nsec->owner); + if (ret == kr_ok()) { + *flags |= FLG_NOEXIST_RRTYPE; + } + return ret <= 0 ? ret : kr_ok(); +} + +/** + * Perform check for RR type wildcard existence denial according to RFC4035 5.4, bullet 1. + * @param flags Flags to be set according to check outcome. + * @param nsec NSEC RR. + * @param sec Packet section to work with. + * @return 0 or error code. + */ +static int no_data_wildcard_existence_check(int *flags, const knot_rrset_t *nsec, + const knot_pktsection_t *sec) +{ + assert(flags && nsec && sec); + + int rrsig_labels = coverign_rrsig_labels(nsec, sec); + if (rrsig_labels < 0) { + return rrsig_labels; + } + int nsec_labels = knot_dname_labels(nsec->owner, NULL); + if (nsec_labels < 0) { + return nsec_labels; + } + + if (rrsig_labels == nsec_labels) { + *flags |= FLG_NOEXIST_WILDCARD; + } + + return kr_ok(); +} + +/** + * Perform check for NSEC wildcard existence that covers sname and + * have no stype bit set. + * @param pkt Packet structure to be processed. + * @param sec Packet section to work with. + * @param sname Queried domain name. + * @param stype Queried type. + * @return 0 or error code. + */ +static int wildcard_match_check(const knot_pkt_t *pkt, const knot_pktsection_t *sec, + const knot_dname_t *sname, uint16_t stype) +{ + if (!sec || !sname) { + return kr_error(EINVAL); + } + + 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_NSEC) { + continue; + } + if (!knot_dname_is_wildcard(rrset->owner)) { + continue; + } + if (!knot_dname_is_equal(rrset->owner, sname)) { + int wcard_labels = knot_dname_labels(rrset->owner, NULL); + int common_labels = knot_dname_matched_labels(rrset->owner, sname); + int rrsig_labels = coverign_rrsig_labels(rrset, sec); + if (wcard_labels < 1 || + common_labels != wcard_labels - 1 || + common_labels != rrsig_labels) { + continue; + } + } + int ret = no_data_response_check_rrtype(&flags, rrset, stype); + if (ret != 0) { + return ret; + } + } + return (flags & FLG_NOEXIST_RRTYPE) ? kr_ok() : kr_error(ENOENT); +} + +int kr_nsec_no_data_response_check(const knot_pkt_t *pkt, knot_section_t section_id, + const knot_dname_t *sname, uint16_t stype) +{ + const knot_pktsection_t *sec = knot_pkt_section(pkt, section_id); + if (!sec || !sname) { + return kr_error(EINVAL); + } + + 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_NSEC) { + continue; + } + if (knot_dname_is_equal(rrset->owner, sname)) { + int ret = no_data_response_check_rrtype(&flags, rrset, stype); + if (ret != 0) { + return ret; + } + } + } + + return (flags & FLG_NOEXIST_RRTYPE) ? kr_ok() : kr_error(ENOENT); +} + +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_existence_denial(const knot_pkt_t *pkt, knot_section_t section_id, + const knot_dname_t *sname, uint16_t stype) +{ + const knot_pktsection_t *sec = knot_pkt_section(pkt, section_id); + if (!sec || !sname) { + return kr_error(EINVAL); + } + + 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_NSEC) { + continue; + } + /* NSEC proves that name exists, but has no data (RFC4035 4.9, 1) */ + if (knot_dname_is_equal(rrset->owner, sname)) { + no_data_response_check_rrtype(&flags, rrset, stype); + } else { + /* NSEC proves that name doesn't exist (RFC4035, 4.9, 2) */ + name_error_response_check_rr(&flags, rrset, sname); + } + no_data_wildcard_existence_check(&flags, rrset, sec); + } + if (kr_nsec_existence_denied(flags)) { + /* denial of existence proved accordignly to 4035 5.4 - + * NSEC proving either rrset non-existance or + * qtype non-existance has been found, + * and no wildcard expansion occurred. + */ + return kr_ok(); + } else if (kr_nsec_rrset_noexist(flags)) { + /* NSEC proving either rrset non-existance or + * qtype non-existance has been found, + * but wildcard expansion occurs. + * Try to find matching wildcard and check + * corresponding types. + */ + return wildcard_match_check(pkt, sec, sname, stype); + } + return kr_error(ENOENT); +} + +int kr_nsec_ref_to_unsigned(const knot_pkt_t *pkt) +{ + int nsec_found = 0; + 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; + } + nsec_found = 0; + for (unsigned j = 0; j < sec->count; ++j) { + const knot_rrset_t *nsec = knot_pkt_rr(sec, j); + if (nsec->type == KNOT_RRTYPE_DS) { + return kr_error(EEXIST); + } + if (nsec->type != KNOT_RRTYPE_NSEC) { + continue; + } + /* nsec found + * check if owner name matches the delegation name + */ + if (!knot_dname_is_equal(nsec->owner, ns->owner)) { + /* nsec does not match the delegation */ + continue; + } + nsec_found = 1; + const uint8_t *bm = knot_nsec_bitmap(nsec->rrs.rdata); + uint16_t bm_size = knot_nsec_bitmap_len(nsec->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)) { + /* rfc4035, 5.2 */ + return kr_ok(); + } + } + if (nsec_found) { + /* nsec which owner matches + * the delegation name was found, + * but nsec type bitmap contains wrong types + */ + return kr_error(EINVAL); + } else { + /* nsec that matches delegation was not found */ + return kr_error(DNSSEC_NOT_FOUND); + } + } + + return kr_error(EINVAL); +} + +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 don't (currently) only use this API for NS. See RFC 6840 sec. 4. + */ + if (type != KNOT_RRTYPE_NS || !nsec || !name) { + assert(!EINVAL); + 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 (!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/nsec.h b/lib/dnssec/nsec.h new file mode 100644 index 0000000..1a2a092 --- /dev/null +++ b/lib/dnssec/nsec.h @@ -0,0 +1,93 @@ +/* Copyright (C) 2015-2017 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include <libknot/packet/pkt.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); + +/** + * Name error response check (RFC4035 3.1.3.2; RFC4035 5.4, bullet 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_nsec_name_error_response_check(const knot_pkt_t *pkt, knot_section_t section_id, + const knot_dname_t *sname); + +/** + * No data response check (RFC4035 3.1.3.1; RFC4035 5.4, bullet 1). + * @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. + */ +int kr_nsec_no_data_response_check(const knot_pkt_t *pkt, knot_section_t section_id, + const knot_dname_t *sname, uint16_t stype); + +/** + * 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); + +/** + * Authenticated denial of existence according to RFC4035 5.4. + * @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. + */ +int kr_nsec_existence_denial(const knot_pkt_t *pkt, knot_section_t section_id, + const knot_dname_t *sname, uint16_t stype); + +/** + * Referral to unsigned subzone check (RFC4035 5.2). + * @note No RRSIGs are validated. + * @param pkt Packet structure to be processed. + * @return 0 or error code: + * DNSSEC_NOT_FOUND - neither ds nor nsec records + * were not found. + * EEXIST - ds record was found. + * EINVAL - bogus. + */ +int kr_nsec_ref_to_unsigned(const knot_pkt_t *pkt); + +/** + * 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..e9e536a --- /dev/null +++ b/lib/dnssec/nsec3.c @@ -0,0 +1,768 @@ +/* Copyright (C) 2015-2017 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include <assert.h> +#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" + +#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) +{ + assert(params && nsec3); + + const knot_rdata_t *rr = knot_rdataset_at(&nsec3->rrs, 0); + assert(rr); + + /* 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) +{ + assert(hash && params); + if (!name) + return kr_error(EINVAL); + if (params->iterations > KR_NSEC3_MAX_ITERATIONS) { + assert(false); // 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) +{ + assert(hash && nsec3); + assert(hash->data); + + 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) +{ + assert(flags && nsec3 && name && skipped); + + 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; + + 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) +{ + assert(flags && nsec3 && name); + + 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 *ownrd = owner_hash.data; + const uint8_t *nextd = next_hash; + int covered = 0; + int greater_then_owner = (memcmp(ownrd, name_hash.data, next_size) < 0); + int less_then_next = (memcmp(name_hash.data, nextd, next_size) < 0); + if (memcmp(ownrd, 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) +{ + assert(nsec3 && name); + + 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) +{ + assert(maxlen >= 3); + 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) { + assert(next_closer[0]); + 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; + } + assert(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) { + assert(sname[0]); + 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; + } + 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; + } + + assert(encloser_name && covering_next_nsec3); + 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 unsecure delegation. + * Denial of existence can not be proven. + * Set error code to proceed unsecure. + */ + 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; + } + + int flags = 0; + 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); + } + if (flags & FLG_NAME_MATCHED) { + /* nsec3 which owner matches + * the delegation name was found, + * but nsec3 type bitmap contains wrong types + */ + return kr_error(EINVAL); + } + /* 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 (type != KNOT_RRTYPE_NS) { + assert(!EINVAL); + 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..1e316f5 --- /dev/null +++ b/lib/dnssec/nsec3.h @@ -0,0 +1,82 @@ +/* Copyright (C) 2015-2017 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include <libknot/packet/pkt.h> + +/** High numbers in NSEC3 iterations don't really help security + * + * ...so we avoid doing all the work. The value is a current compromise; + * zones shooting over get downgraded to insecure status. + * + * Original restriction wasn't that strict: + https://datatracker.ietf.org/doc/html/rfc5155#section-10.3 + * but there is discussion about officially lowering the limits: + https://tools.ietf.org/id/draft-hardaker-dnsop-nsec3-guidance-02.html#section-2.3 + */ +#define KR_NSEC3_MAX_ITERATIONS 150 + +/** + * 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. + */ +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..fed0cf4 --- /dev/null +++ b/lib/dnssec/signature.c @@ -0,0 +1,300 @@ +/* Copyright (C) 2015-2017 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include <arpa/inet.h> +#include <assert.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 "lib/defines.h" +#include "lib/utils.h" +#include "lib/dnssec/signature.h" + +#include "contrib/wire.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) +{ + assert(ref && key); + if (ref->type != KNOT_RRTYPE_DS) { + return kr_error(EINVAL); + } + + /* Try all possible DS records */ + int ret = 0; + knot_rdata_t *rd = ref->rrs.rdata; + for (uint16_t i = 0; i < ref->rrs.count; ++i) { + dnssec_binary_t ds_rdata = { + .size = rd->len, + .data = rd->data + }; + ret = authenticate_ds(key, &ds_rdata, knot_ds_digest_type(rd)); + if (ret == 0) { /* Found a good DS */ + return kr_ok(); + } + rd = knot_rdataset_next(rd); + } + + 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) +{ + assert(wire); + 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; + + assert(i <= wire_size); + } + + 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) +{ + assert(ctx); + assert(rdata); + + 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) { + assert(beginp[0]); + beginp = (uint8_t *) knot_wire_next_label(beginp, NULL); + assert(beginp != NULL); + } + *(--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 = 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, + #if KNOT_VERSION_MAJOR >= 3 + false, + #endif + &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..9e71ad9 --- /dev/null +++ b/lib/dnssec/signature.h @@ -0,0 +1,29 @@ +/* Copyright (C) 2015-2017 CZ.NIC, z.s.p.o. <knot-dns@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 RRSet containing signatures. + * @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..b6a8cb0 --- /dev/null +++ b/lib/dnssec/ta.c @@ -0,0 +1,173 @@ +/* Copyright (C) 2014-2017 CZ.NIC, z.s.p.o. <knot-dns@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(map_t *trust_anchors, const knot_dname_t *name) +{ + return map_get(trust_anchors, (const char *)name); +} + +const knot_dname_t *kr_ta_get_longest_name(map_t *trust_anchors, const knot_dname_t *name) +{ + while(name) { + if (kr_ta_get(trust_anchors, name)) { + return name; + } + if (name[0] == '\0') { + break; + } + 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_info("[ 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(map_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) { + return map_set(trust_anchors, (const char *)name, ta_rr); + } + return kr_ok(); +} + +int kr_ta_add(map_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/DNSEY 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); + } +} + +int kr_ta_covers(map_t *trust_anchors, const knot_dname_t *name) +{ + while(name) { + if (kr_ta_get(trust_anchors, name)) { + return true; + } + if (name[0] == '\0') { + return false; + } + name = knot_wire_next_label(name, NULL); + } + return false; +} + +bool kr_ta_covers_qry(struct kr_context *ctx, const knot_dname_t *name, + const uint16_t type) +{ + assert(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); + if (!name) { + assert(false); + return false; + } + } + return kr_ta_covers(&ctx->trust_anchors, name) + && !kr_ta_covers(&ctx->negative_anchors, name); +} + +/* Delete record data */ +static int del_record(const char *k, void *v, void *ext) +{ + knot_rrset_t *ta_rr = v; + if (ta_rr) { + knot_rrset_free(ta_rr, NULL); + } + return 0; +} + +int kr_ta_del(map_t *trust_anchors, const knot_dname_t *name) +{ + knot_rrset_t *ta_rr = kr_ta_get(trust_anchors, name); + if (ta_rr) { + del_record(NULL, ta_rr, NULL); + map_del(trust_anchors, (const char *)name); + } + return kr_ok(); +} + +void kr_ta_clear(map_t *trust_anchors) +{ + map_walk(trust_anchors, del_record, NULL); + map_clear(trust_anchors); +} diff --git a/lib/dnssec/ta.h b/lib/dnssec/ta.h new file mode 100644 index 0000000..f8fc93a --- /dev/null +++ b/lib/dnssec/ta.h @@ -0,0 +1,75 @@ +/* Copyright (C) 2015-2017 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "lib/generic/map.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(map_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(map_t *trust_anchors, const knot_dname_t *name, uint16_t type, + uint32_t ttl, const uint8_t *rdata, uint16_t rdlen); + +/** + * Return true if the name is below/at any TA in the store. + * This can be useful to check if it's possible to validate a name beforehand. + * @param trust_anchors trust store + * @param name name of the TA + * @return boolean + */ +KR_EXPORT KR_PURE +int kr_ta_covers(map_t *trust_anchors, const knot_dname_t *name); + +struct kr_context; +/** + * A wrapper around kr_ta_covers that is aware of negative TA and types. + */ +KR_EXPORT KR_PURE +bool kr_ta_covers_qry(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(map_t *trust_anchors, const knot_dname_t *name); + +/** + * Clear trust store. + * @param trust_anchors trust store + */ +KR_EXPORT +void kr_ta_clear(map_t *trust_anchors); + +/** + * Return TA with the longest name that covers given name. + * @param trust_anchors trust store + * @param name name of the TA + * @return pointer to name or NULL. + if not NULL, points inside the name parameter. + */ +KR_EXPORT +const knot_dname_t *kr_ta_get_longest_name(map_t *trust_anchors, const knot_dname_t *name); |