summaryrefslogtreecommitdiffstats
path: root/lib/dnssec
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 10:41:58 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 10:41:58 +0000
commit1852910ef0fd7393da62b88aee66ee092208748e (patch)
treead3b659dbbe622b58a5bda4fe0b5e1d80eee9277 /lib/dnssec
parentInitial commit. (diff)
downloadknot-resolver-upstream.tar.xz
knot-resolver-upstream.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.c528
-rw-r--r--lib/dnssec/nsec.h93
-rw-r--r--lib/dnssec/nsec3.c768
-rw-r--r--lib/dnssec/nsec3.h82
-rw-r--r--lib/dnssec/signature.c300
-rw-r--r--lib/dnssec/signature.h29
-rw-r--r--lib/dnssec/ta.c173
-rw-r--r--lib/dnssec/ta.h75
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(&params, 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, &params, 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(&params);
+ }
+ 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(&params, nsec3);
+ if (ret != 0) {
+ goto fail;
+ }
+
+ ret = hash_name(&name_hash, &params, 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(&params);
+ }
+ 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(&params, nsec3);
+ if (ret != 0) {
+ goto fail;
+ }
+
+ ret = hash_name(&name_hash, &params, 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(&params);
+ }
+ 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);