From f449f278dd3c70e479a035f50a9bb817a9b433ba Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 17:24:08 +0200 Subject: Adding upstream version 3.2.6. Signed-off-by: Daniel Baumann --- src/knot/nameserver/nsec_proofs.c | 677 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 677 insertions(+) create mode 100644 src/knot/nameserver/nsec_proofs.c (limited to 'src/knot/nameserver/nsec_proofs.c') diff --git a/src/knot/nameserver/nsec_proofs.c b/src/knot/nameserver/nsec_proofs.c new file mode 100644 index 0000000..71944b1 --- /dev/null +++ b/src/knot/nameserver/nsec_proofs.c @@ -0,0 +1,677 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +#include + +#include "libknot/libknot.h" +#include "knot/nameserver/nsec_proofs.h" +#include "knot/nameserver/internet.h" +#include "knot/dnssec/zone-nsec.h" + +/*! + * \brief Check if node is empty non-terminal. + */ +static bool empty_nonterminal(const zone_node_t *node) +{ + return node && node->rrset_count == 0; +} + +/*! + * \brief Check if wildcard expansion happened for given node and QNAME. + */ +static bool wildcard_expanded(const zone_node_t *node, const knot_dname_t *qname) +{ + return !knot_dname_is_wildcard(qname) && knot_dname_is_wildcard(node->owner); +} + +/*! + * \brief Check if opt-out can take an effect. + */ +static bool ds_optout(const zone_node_t *node) +{ + return node_nsec3_get(node) == NULL && !(node->flags & NODE_FLAGS_SUBTREE_AUTH); +} + +/*! + * \brief Check if node is part of the NSEC chain. + * + * NSEC is created for each node with authoritative data or delegation. + * + * \see https://tools.ietf.org/html/rfc4035#section-2.3 + */ +static bool node_in_nsec(const zone_node_t *node) +{ + return (node->flags & NODE_FLAGS_NONAUTH) == 0 && !empty_nonterminal(node); +} + +/*! + * \brief Check if node is part of the NSEC3 chain. + * + * NSEC3 is created for each node with authoritative data, empty-non terminal, + * and delegation (unless opt-out is in effect). + * + * \see https://tools.ietf.org/html/rfc5155#section-7.1 + */ +static bool node_in_nsec3(const zone_node_t *node) +{ + return (node->flags & NODE_FLAGS_NONAUTH) == 0 && !ds_optout(node); +} + +/*! + * \brief Walk previous names until we reach a node in NSEC chain. + * + */ +static const zone_node_t *nsec_previous(const zone_node_t *previous) +{ + assert(previous); + + while (!node_in_nsec(previous)) { + previous = node_prev(previous); + assert(previous); + } + + return previous; +} + +/*! + * \brief Get closest provable encloser from closest matching parent node. + */ +static const zone_node_t *nsec3_encloser(const zone_node_t *closest) +{ + assert(closest); + + while (!node_in_nsec3(closest)) { + closest = node_parent(closest); + assert(closest); + } + + return closest; +} + +/*! + * \brief Create a 'next closer name' to the given domain name. + * + * Next closer is the name one label longer than the closest provable encloser + * of a name. + * + * \see https://tools.ietf.org/html/rfc5155#section-1.3 + * + * \param closest_encloser Closest provable encloser of \a name. + * \param name Domain name to create the 'next closer' name to. + * + * \return Next closer name, NULL on error. + */ +static const knot_dname_t *get_next_closer(const knot_dname_t *closest_encloser, + const knot_dname_t *name) +{ + // make name only one label longer than closest_encloser + size_t ce_labels = knot_dname_labels(closest_encloser, NULL); + size_t qname_labels = knot_dname_labels(name, NULL); + for (int i = 0; i < (qname_labels - ce_labels - 1); ++i) { + name = knot_wire_next_label(name, NULL); + } + + // the common labels should match + assert(knot_dname_is_equal(knot_wire_next_label(name, NULL), closest_encloser)); + + return name; +} + +/*! + * \brief Put NSEC/NSEC3 record with corresponding RRSIG into the response. + */ +static int put_nxt_from_node(const zone_node_t *node, + uint16_t type, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + assert(type == KNOT_RRTYPE_NSEC || type == KNOT_RRTYPE_NSEC3); + + knot_rrset_t rrset = node_rrset(node, type); + if (knot_rrset_empty(&rrset)) { + return KNOT_EOK; + } + + knot_rrset_t rrsigs = node_rrset(node, KNOT_RRTYPE_RRSIG); + + return process_query_put_rr(resp, qdata, &rrset, &rrsigs, + KNOT_COMPR_HINT_NONE, KNOT_PF_CHECKDUP); +} + +/*! + * \brief Put NSEC record with corresponding RRSIG into the response. + */ +static int put_nsec_from_node(const zone_node_t *node, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + return put_nxt_from_node(node, KNOT_RRTYPE_NSEC, qdata, resp); +} + +/*! + * \brief Put NSEC3 record with corresponding RRSIG into the response. + */ +static int put_nsec3_from_node(const zone_node_t *node, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + return put_nxt_from_node(node, KNOT_RRTYPE_NSEC3, qdata, resp); +} + +/*! + * \brief Find NSEC for given name and put it into the response. + * + * Note this function allows the name to match the QNAME. The NODATA proof + * for empty non-terminal is equivalent to NXDOMAIN proof, except that the + * names may exist. This is why. + */ +static int put_covering_nsec(const zone_contents_t *zone, + const knot_dname_t *name, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + const zone_node_t *match = NULL; + const zone_node_t *closest = NULL; + const zone_node_t *prev = NULL; + + const zone_node_t *proof = NULL; + + int ret = zone_contents_find_dname(zone, name, &match, &closest, &prev); + if (ret == ZONE_NAME_FOUND) { + proof = match; + } else if (ret == ZONE_NAME_NOT_FOUND) { + proof = nsec_previous(prev); + } else { + assert(ret < 0); + return ret; + } + + return put_nsec_from_node(proof, qdata, resp); +} + +/*! + * \brief Find NSEC3 covering the given name and put it into the response. + */ +static int put_covering_nsec3(const zone_contents_t *zone, + const knot_dname_t *name, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + const zone_node_t *prev = NULL; + const zone_node_t *node = NULL; + + int match = zone_contents_find_nsec3_for_name(zone, name, &node, &prev); + if (match < 0) { + // ignore if missing + return KNOT_EOK; + } + + if (match == ZONE_NAME_FOUND || prev == NULL){ + return KNOT_ERROR; + } + + return put_nsec3_from_node(prev, qdata, resp); +} + +/*! + * \brief Add NSEC3 covering the next closer name to closest encloser. + * + * \param cpe Closest provable encloser of \a qname. + * \param qname Source QNAME. + * \param zone Source zone. + * \param qdata Query processing data. + * \param resp Response packet. + * + * \return KNOT_E* + */ +static int put_nsec3_next_closer(const zone_node_t *cpe, + const knot_dname_t *qname, + const zone_contents_t *zone, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + const knot_dname_t *next_closer = get_next_closer(cpe->owner, qname); + + return put_covering_nsec3(zone, next_closer, qdata, resp); +} + +/*! + * \brief Add NSEC3s for closest encloser proof. + * + * Adds up to two NSEC3 records. The first one proves that closest encloser + * of the queried name exists, the second one proves that the name bellow the + * encloser doesn't. + * + * \see https://tools.ietf.org/html/rfc5155#section-7.2.1 + * + * \param qname Source QNAME. + * \param zone Source zone. + * \param cpe Closest provable encloser of \a qname. + * \param qdata Query processing data. + * \param resp Response packet. + * + * \return KNOT_E* + */ +static int put_closest_encloser_proof(const knot_dname_t *qname, + const zone_contents_t *zone, + const zone_node_t *cpe, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + // An NSEC3 RR that matches the closest (provable) encloser. + + int ret = put_nsec3_from_node(node_nsec3_get(cpe), qdata, resp); + if (ret != KNOT_EOK) { + return ret; + } + + // An NSEC3 RR that covers the "next closer" name to the closest encloser. + + return put_nsec3_next_closer(cpe, qname, zone, qdata, resp); +} + +/*! + * \brief Put NSEC for wildcard answer into the response. + * + * Add NSEC record proving that no better match on QNAME exists. + * + * \see https://tools.ietf.org/html/rfc4035#section-3.1.3.3 + * + * \param previous Previous name for QNAME. + * \param qdata Query processing data. + * \param resp Response packet. + * + * \return KNOT_E* + */ +static int put_nsec_wildcard(const zone_node_t *previous, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + return put_nsec_from_node(previous, qdata, resp); +} + +/*! + * \brief Put NSEC3s for wildcard answer into the response. + * + * Add NSEC3 record proving that no better match on QNAME exists. + * + * \see https://tools.ietf.org/html/rfc5155#section-7.2.6 + * + * \param wildcard Wildcard node that was used for expansion. + * \param qname Source QNAME. + * \param zone Source zone. + * \param qdata Query processing data. + * \param resp Response packet. + */ +static int put_nsec3_wildcard(const zone_node_t *wildcard, + const knot_dname_t *qname, + const zone_contents_t *zone, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + const zone_node_t *cpe = nsec3_encloser(node_parent(wildcard)); + + return put_nsec3_next_closer(cpe, qname, zone, qdata, resp); +} + +/*! + * \brief Put NSECs or NSEC3s for wildcard expansion in the response. + * + * \return KNOT_E* + */ +static int put_wildcard_answer(const zone_node_t *wildcard, + const zone_node_t *previous, + const zone_contents_t *zone, + const knot_dname_t *qname, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + if (!wildcard_expanded(wildcard, qname)) { + return KNOT_EOK; + } + + int ret = 0; + + if (knot_is_nsec3_enabled(zone)) { + ret = put_nsec3_wildcard(wildcard, qname, zone, qdata, resp); + } else { + previous = nsec_previous(previous); + ret = put_nsec_wildcard(previous, qdata, resp); + } + + return ret; +} + +/*! + * \brief Put NSECs for NXDOMAIN error into the response. + * + * Adds up to two NSEC records. We have to prove that the queried name doesn't + * exist and that no wildcard expansion is possible for that name. + * + * \see https://tools.ietf.org/html/rfc4035#section-3.1.3.2 + * + * \param zone Source zone. + * \param previous Previous node to QNAME. + * \param closest Closest matching parent of QNAME. + * \param qdata Query data. + * \param resp Response packet. + * + * \return KNOT_E* + */ +static int put_nsec_nxdomain(const zone_contents_t *zone, + const zone_node_t *previous, + const zone_node_t *closest, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + assert(previous); + assert(closest); + + // An NSEC RR proving that there is no exact match for . + + previous = nsec_previous(previous); + int ret = put_nsec_from_node(previous, qdata, resp); + if (ret != KNOT_EOK) { + return ret; + } + + // An NSEC RR proving that the zone contains no RRsets that would match + // via wildcard name expansion. + + // NOTE: closest may be empty non-terminal and thus not authoritative. + + size_t size = knot_dname_size(closest->owner); + if (size > KNOT_DNAME_MAXLEN - 2) { + return KNOT_EINVAL; + } + assert(size > 0); + uint8_t wildcard[2 + size]; + memcpy(wildcard, "\x01""*", 2); + memcpy(wildcard + 2, closest->owner, size); + + return put_covering_nsec(zone, wildcard, qdata, resp); +} + +/*! + * \brief Put NSEC3s for NXDOMAIN error into the response. + * + * Adds up to three NSEC3 records. We have to prove that some parent name + * exists (closest encloser proof) and that no wildcard expansion is possible + * bellow that closest encloser. + * + * \see https://tools.ietf.org/html/rfc5155#section-7.2.2 + * + * \param qname Source QNAME. + * \param zone Source zone. + * \param closest Closest matching parent of \a qname. + * \param qdata Query processing data. + * \param resp Response packet. + * + * \retval KNOT_E* + */ +static int put_nsec3_nxdomain(const knot_dname_t *qname, + const zone_contents_t *zone, + const zone_node_t *closest, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + const zone_node_t *cpe = nsec3_encloser(closest); + + // Closest encloser proof. + + int ret = put_closest_encloser_proof(qname, zone, cpe, qdata, resp); + if (ret != KNOT_EOK) { + return ret; + } + + // NSEC3 covering the (nonexistent) wildcard at the closest encloser. + + const zone_node_t *nsec3_wildcard_prev, *ignored; + if (cpe->nsec3_wildcard_name == NULL || + zone_contents_find_nsec3(zone, cpe->nsec3_wildcard_name, &ignored, &nsec3_wildcard_prev) == ZONE_NAME_FOUND) { + return KNOT_ERROR; + } + + return put_nsec3_from_node(nsec3_wildcard_prev, qdata, resp); +} + +/*! + * \brief Put NSECs or NSEC3s for the NXDOMAIN error into the response. + * + * \param zone Zone used for answering. + * \param previous Previous node to \a qname. + * \param closest Closest matching parent name for \a qname. + * \param qname Source QNAME. + * \param qdata Query processing data. + * \param resp Response packet. + * + * \return KNOT_E* + */ +static int put_nxdomain(const zone_contents_t *zone, + const zone_node_t *previous, + const zone_node_t *closest, + const knot_dname_t *qname, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + if (knot_is_nsec3_enabled(zone)) { + return put_nsec3_nxdomain(qname, zone, closest, qdata, resp); + } else { + return put_nsec_nxdomain(zone, previous, closest, qdata, resp); + } +} + +/*! + * \brief Put NSEC for NODATA error into the response. + * + * Then NSEC matching the QNAME must be added into the response and the bitmap + * will indicate that the QTYPE doesn't exist. As NSECs for empty non-terminals + * don't exist, the proof for NODATA match on non-terminal is proved like + * non-existence of the queried name. + * + * \see https://tools.ietf.org/html/rfc4035#section-3.1.3.1 + * + * \param match Node matching QNAME. + * \param previous Previous node to QNAME in the zone. + * \param qdata Query processing data. + * \param resp Response packet. + * + * \return KNOT_E* + */ +static int put_nsec_nodata(const zone_node_t *match, + const zone_node_t *previous, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + if (empty_nonterminal(match)) { + return put_nsec_from_node(nsec_previous(previous), qdata, resp); + } else { + return put_nsec_from_node(match, qdata, resp); + } +} + +/*! + * \brief Put NSEC3 for NODATA error into the response. + * + * The NSEC3 matching the QNAME is added into the response and the bitmap + * will indicate that the QTYPE doesn't exist. For QTYPE==DS, the server + * may alternatively serve a closest encloser proof with opt-out. For wildcard + * expansion, the closest encloser proof must included as well. + * + * \see https://tools.ietf.org/html/rfc5155#section-7.2.3 + * \see https://tools.ietf.org/html/rfc5155#section-7.2.4 + * \see https://tools.ietf.org/html/rfc5155#section-7.2.5 + */ +static int put_nsec3_nodata(const knot_dname_t *qname, + const zone_contents_t *zone, + const zone_node_t *match, + const zone_node_t *closest, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + int ret = KNOT_EOK; + + // NSEC3 matching QNAME is always included. + + zone_node_t *nsec3_match = node_nsec3_get(match); + if (nsec3_match != NULL) { + ret = put_nsec3_from_node(nsec3_match, qdata, resp); + if (ret != KNOT_EOK) { + return ret; + } + } + + // Closest encloser proof for wildcard effect or NSEC3 opt-out. + + if (wildcard_expanded(match, qname) || ds_optout(match)) { + const zone_node_t *cpe = nsec3_encloser(closest); + ret = put_closest_encloser_proof(qname, zone, cpe, qdata, resp); + } + + return ret; +} + +/*! + * \brief Put NSECs or NSEC3s for the NODATA error into the response. + * + * \param node Source node. + * \param qdata Query processing data. + * \param resp Response packet. + */ +static int put_nodata(const zone_node_t *node, + const zone_node_t *closest, + const zone_node_t *previous, + const zone_contents_t *zone, + const knot_dname_t *qname, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + if (knot_is_nsec3_enabled(zone)) { + return put_nsec3_nodata(qname, zone, node, closest, qdata, resp); + } else { + return put_nsec_nodata(node, previous, qdata, resp); + } +} + +int nsec_prove_wildcards(knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + if (qdata->extra->contents == NULL) { + return KNOT_EINVAL; + } + + int ret = KNOT_EOK; + struct wildcard_hit *item; + + WALK_LIST(item, qdata->extra->wildcards) { + if (item->node == NULL) { + return KNOT_EINVAL; + } + ret = put_wildcard_answer(item->node, item->prev, + qdata->extra->contents, + item->sname, qdata, pkt); + if (ret != KNOT_EOK) { + break; + } + } + + return ret; +} + +int nsec_prove_nodata(knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + if (qdata->extra->contents == NULL || qdata->extra->node == NULL) { + return KNOT_EINVAL; + } + + return put_nodata(qdata->extra->node, qdata->extra->encloser, qdata->extra->previous, + qdata->extra->contents, qdata->name, qdata, pkt); +} + +int nsec_prove_nxdomain(knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + if (qdata->extra->contents == NULL) { + return KNOT_EINVAL; + } + + return put_nxdomain(qdata->extra->contents, + qdata->extra->previous, qdata->extra->encloser, + qdata->name, qdata, pkt); +} + +int nsec_prove_dp_security(knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + if (qdata->extra->node == NULL || qdata->extra->encloser == NULL || + qdata->extra->contents == NULL) { + return KNOT_EINVAL; + } + + // Add DS into the response. + + knot_rrset_t rrset = node_rrset(qdata->extra->node, KNOT_RRTYPE_DS); + if (!knot_rrset_empty(&rrset)) { + knot_rrset_t rrsigs = node_rrset(qdata->extra->node, KNOT_RRTYPE_RRSIG); + return process_query_put_rr(pkt, qdata, &rrset, &rrsigs, + KNOT_COMPR_HINT_NONE, 0); + } + + // Alternatively prove that DS doesn't exist. + + return put_nodata(qdata->extra->node, qdata->extra->encloser, qdata->extra->previous, + qdata->extra->contents, qdata->name, qdata, pkt); +} + +int nsec_append_rrsigs(knot_pkt_t *pkt, knotd_qdata_t *qdata, bool optional) +{ + int ret = KNOT_EOK; + uint16_t flags = optional ? KNOT_PF_NOTRUNC : KNOT_PF_NULL; + flags |= KNOT_PF_FREE; // Free all RRSIGs, they are synthesized + flags |= KNOT_PF_ORIGTTL; + + /* Append RRSIGs for section. */ + struct rrsig_info *info; + WALK_LIST(info, qdata->extra->rrsigs) { + knot_rrset_t *rrsig = &info->synth_rrsig; + uint16_t compr_hint = info->rrinfo->compress_ptr[KNOT_COMPR_HINT_OWNER]; + uint16_t flags_mask = (info->rrinfo->flags & KNOT_PF_SOAMINTTL) ? KNOT_PF_ORIGTTL : 0; + ret = knot_pkt_put(pkt, compr_hint, rrsig, flags & ~flags_mask); + if (ret != KNOT_EOK) { + break; + } + /* RRSIG is owned by packet now. */ + knot_rdataset_init(&info->synth_rrsig.rrs); + }; + + /* Clear the list. */ + nsec_clear_rrsigs(qdata); + + return ret; +} + +void nsec_clear_rrsigs(knotd_qdata_t *qdata) +{ + if (qdata == NULL) { + return; + } + + struct rrsig_info *info; + WALK_LIST(info, qdata->extra->rrsigs) { + knot_rrset_t *rrsig = &info->synth_rrsig; + knot_rrset_clear(rrsig, qdata->mm); + }; + + ptrlist_free(&qdata->extra->rrsigs, qdata->mm); + init_list(&qdata->extra->rrsigs); +} -- cgit v1.2.3