Commit c04f4f38 authored by Daniel Salzman's avatar Daniel Salzman

Merge branch 'nsec-proofs-refactoring' into 'master'

NSEC proofs refactoring

Close #191, #471

See merge request !570
parents fe731eaa a7d2f8cf
......@@ -49,7 +49,8 @@ static int wildcard_has_visited(struct query_data *qdata, const zone_node_t *nod
}
/*! \brief Mark given node as visited. */
static int wildcard_visit(struct query_data *qdata, const zone_node_t *node, const knot_dname_t *sname)
static int wildcard_visit(struct query_data *qdata, const zone_node_t *node,
const zone_node_t *prev, const knot_dname_t *sname)
{
assert(qdata);
assert(node);
......@@ -62,6 +63,7 @@ static int wildcard_visit(struct query_data *qdata, const zone_node_t *node, con
knot_mm_t *mm = qdata->mm;
struct wildcard_hit *item = mm_alloc(mm, sizeof(struct wildcard_hit));
item->node = node;
item->prev = prev;
item->sname = sname;
add_tail(&qdata->wildcards, (node_t *)item);
return KNOT_EOK;
......@@ -388,7 +390,7 @@ static int follow_cname(knot_pkt_t *pkt, uint16_t rrtype, struct query_data *qda
}
/* Put to wildcard node list. */
if (wildcard_visit(qdata, cname_node, qdata->name) != KNOT_EOK) {
if (wildcard_visit(qdata, cname_node, qdata->previous, qdata->name) != KNOT_EOK) {
return ERROR;
}
}
......@@ -448,14 +450,11 @@ static int name_not_found(knot_pkt_t *pkt, struct query_data *qdata)
qdata->node = wildcard_node;
assert(qdata->node != NULL);
/* keep encloser */
qdata->previous = NULL;
/* Follow expanded wildcard. */
int next_state = name_found(pkt, qdata);
/* Put to wildcard node list. */
if (wildcard_visit(qdata, wildcard_node, qdata->name) != KNOT_EOK) {
if (wildcard_visit(qdata, wildcard_node, qdata->previous, qdata->name) != KNOT_EOK) {
next_state = ERROR;
}
......
......@@ -21,182 +21,122 @@
#include "knot/nameserver/internet.h"
#include "knot/dnssec/zone-nsec.h"
/*! \note #191 There is a lot of duplicate and legacy code here. I have just
* divided the API into 3 + 1 basic proofs used and separated the
* code to its own file. Still, it should be cleaned up and
* each proof should be very briefly documented (what proves what)
* with hints to the RFC, as it's not so complicated as it looks here.
/*!
* \brief Check if node is empty non-terminal.
*/
static bool empty_nonterminal(const zone_node_t *node)
{
return node && node->rrset_count == 0;
}
/*!
* \brief Creates a 'next closer name' to the given domain name.
*
* For definition of 'next closer name', see RFC5155, Page 6.
*
* \param closest_encloser Closest encloser of \a name.
* \param name Domain name to create the 'next closer' name to.
*
* \return 'Next closer name' to the given domain name or NULL if an error
* occurred.
* \brief Check if wildcard expansion happened for given node and QNAME.
*/
static knot_dname_t *get_next_closer(const knot_dname_t *closest_encloser,
const knot_dname_t *name)
static bool wildcard_expanded(const zone_node_t *node, const knot_dname_t *qname)
{
int ce_labels = knot_dname_labels(closest_encloser, NULL);
int qname_labels = knot_dname_labels(name, NULL);
// the common labels should match
assert(knot_dname_matched_labels(closest_encloser, name) == ce_labels);
return !knot_dname_is_wildcard(qname) && knot_dname_is_wildcard(node->owner);
}
// chop some labels from the qname
for (int i = 0; i < (qname_labels - ce_labels - 1); ++i) {
name = knot_wire_next_label(name, NULL);
}
/*!
* \brief Check if opt-out can take an effect.
*/
static bool ds_optout(const zone_node_t *node)
{
return node->nsec3_node == NULL && node->flags & NODE_FLAGS_DELEG;
}
return knot_dname_copy(name, NULL);
/*!
* \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 Adds NSEC3 RRSet (together with corresponding RRSIGs) from the given
* node into the response.
* \brief Check if node is part of the NSEC3 chain.
*
* \param node Node to get the NSEC3 RRSet from.
* \param resp Response where to add the RRSets.
* 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 int put_nsec3_from_node(const zone_node_t *node,
struct query_data *qdata,
knot_pkt_t *resp)
static bool node_in_nsec3(const zone_node_t *node)
{
knot_rrset_t rrset = node_rrset(node, KNOT_RRTYPE_NSEC3);
knot_rrset_t rrsigs = node_rrset(node, KNOT_RRTYPE_RRSIG);
if (knot_rrset_empty(&rrset)) {
// bad zone, ignore
return KNOT_EOK;
}
return (node->flags & NODE_FLAGS_NONAUTH) == 0 && !ds_optout(node);
}
int res = ns_put_rr(resp, &rrset, &rrsigs, KNOT_COMPR_HINT_NONE,
KNOT_PF_CHECKDUP, qdata);
/*!
* \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);
/*! \note TC bit is already set, if something went wrong. */
while (!node_in_nsec(previous)) {
previous = previous->prev;
assert(previous);
}
// return the error code, so that other code may be skipped
return res;
return previous;
}
/*!
* \brief Finds and adds NSEC3 covering the given domain name (and their
* associated RRSIGs) to the response.
*
* \param zone Zone used for answering.
* \param name Domain name to cover.
* \param resp Response where to add the RRSets.
*
* \retval KNOT_EOK
* \retval NS_ERR_SERVFAIL if a runtime collision occurred. The server should
* respond with SERVFAIL in such case.
* \brief Get closest provable encloser from closest matching parent node.
*/
static int put_covering_nsec3(const zone_contents_t *zone,
const knot_dname_t *name,
struct query_data *qdata,
knot_pkt_t *resp)
static const zone_node_t *nsec3_encloser(const zone_node_t *closest)
{
const zone_node_t *prev, *node;
/*! \todo Check version. */
int match = zone_contents_find_nsec3_for_name(zone, name, &node, &prev);
if (match < 0) {
// ignoring, what can we do anyway?
return KNOT_EOK;
}
assert(closest);
if (match == ZONE_NAME_FOUND || prev == NULL){
// if run-time collision => SERVFAIL
return KNOT_EOK;
while (!node_in_nsec3(closest)) {
closest = closest->parent;
assert(closest);
}
return put_nsec3_from_node(prev, qdata, resp);
return closest;
}
/*!
* \brief Adds NSEC3s comprising the 'closest encloser proof' for the given
* (non-existent) domain name (and their associated RRSIGs) to the
* response.
* \brief Create a 'next closer name' to the given domain name.
*
* For definition of 'closest encloser proof', see RFC5155, section 7.2.1,
* Page 18.
* Next closer is the name one label longer than the closest provable encloser
* of a name.
*
* \note This function does not check if DNSSEC is enabled, nor if it is
* requested by the query.
* \see https://tools.ietf.org/html/rfc5155#section-1.3
*
* \param zone Zone used for answering.
* \param closest_encloser Closest encloser of \a qname in the zone.
* \param qname Searched (non-existent) name.
* \param resp Response where to add the NSEC3s.
* \param closest_encloser Closest provable encloser of \a name.
* \param name Domain name to create the 'next closer' name to.
*
* \retval KNOT_EOK
* \retval NS_ERR_SERVFAIL
* \return Next closer name, NULL on error.
*/
static int put_closest_encloser_proof(const zone_contents_t *zone,
const zone_node_t **closest_encloser,
const knot_dname_t *qname,
struct query_data *qdata,
knot_pkt_t *resp)
static knot_dname_t *get_next_closer(const knot_dname_t *closest_encloser,
const knot_dname_t *name)
{
assert(zone != NULL);
assert(knot_is_nsec3_enabled(zone));
assert(closest_encloser != NULL);
assert(*closest_encloser != NULL);
assert(qname != NULL);
assert(resp != NULL);
/*
* 1) NSEC3 that matches closest provable encloser.
*/
const zone_node_t *nsec3_node = NULL;
const knot_dname_t *next_closer = NULL;
while ((nsec3_node = (*closest_encloser)->nsec3_node) == NULL) {
next_closer = (*closest_encloser)->owner;
*closest_encloser = (*closest_encloser)->parent;
if (*closest_encloser == NULL) {
// there are no NSEC3s to add
return KNOT_EOK;
}
}
assert(nsec3_node != NULL);
int ret = put_nsec3_from_node(nsec3_node, qdata, resp);
if (ret != KNOT_EOK) {
return ret;
}
/*
* 2) NSEC3 that covers the "next closer" name.
*/
if (next_closer == NULL) {
// create the "next closer" name by appending from qname
knot_dname_t *new_next_closer = get_next_closer((*closest_encloser)->owner,
qname);
if (new_next_closer == NULL) {
return KNOT_ERROR; /*servfail */
}
int ce_labels = knot_dname_labels(closest_encloser, NULL);
int qname_labels = knot_dname_labels(name, NULL);
ret = put_covering_nsec3(zone, new_next_closer, qdata, resp);
// the common labels should match
assert(knot_dname_matched_labels(closest_encloser, name) == ce_labels);
knot_dname_free(&new_next_closer, NULL);
} else {
ret = put_covering_nsec3(zone, next_closer, qdata, resp);
// chop some labels from the qname
for (int i = 0; i < (qname_labels - ce_labels - 1); ++i) {
name = knot_wire_next_label(name, NULL);
}
return ret;
return knot_dname_copy(name, NULL);
}
/*!
* \brief Creates a name of a wildcard child of \a name.
* \brief Create a wildcard child of a name.
*
* \param name Domain name to get the wildcard child name of.
* \param name Parent of the wildcard.
*
* \return Wildcard child name or NULL if an error occurred.
* \return Wildcard child name, NULL on error.
*/
static knot_dname_t *wildcard_child_name(const knot_dname_t *name)
{
......@@ -215,319 +155,349 @@ static knot_dname_t *wildcard_child_name(const knot_dname_t *name)
return wildcard;
}
/*!
* \brief Puts NSECs for wildcard answer into the response.
*
* \note This function does not check if DNSSEC is enabled, nor if it is
* requested by the query.
*
* \param zone Zone used for answering.
* \param qname Domain name covered by the wildcard used for answering the
* query.
* \param previous Previous node of \a qname in canonical order.
* \param qdata Query data.
* \param resp Response to put the NSEC3s into.
* \brief Put NSEC/NSEC3 record with corresponding RRSIG into the response.
*/
static int put_nsec_wildcard(const zone_contents_t *zone,
const knot_dname_t *qname,
const zone_node_t *previous,
static int put_nxt_from_node(const zone_node_t *node,
uint16_t type,
struct query_data *qdata,
knot_pkt_t *resp)
{
// check if we have previous; if not, find one using the tree
if (previous == NULL) {
previous = zone_contents_find_previous(zone, qname);
assert(previous != NULL);
assert(type == KNOT_RRTYPE_NSEC || type == KNOT_RRTYPE_NSEC3);
while (previous->flags & NODE_FLAGS_NONAUTH) {
previous = previous->prev;
}
knot_rrset_t rrset = node_rrset(node, type);
if (knot_rrset_empty(&rrset)) {
return KNOT_EOK;
}
knot_rrset_t rrset = node_rrset(previous, KNOT_RRTYPE_NSEC);
int ret = KNOT_EOK;
knot_rrset_t rrsigs = node_rrset(node, KNOT_RRTYPE_RRSIG);
if (!knot_rrset_empty(&rrset)) {
knot_rrset_t rrsigs = node_rrset(previous, KNOT_RRTYPE_RRSIG);
// NSEC proving that there is no node with the searched name
ret = ns_put_rr(resp, &rrset, &rrsigs, KNOT_COMPR_HINT_NONE,
KNOT_PF_CHECKDUP, qdata);
}
return ns_put_rr(resp, &rrset, &rrsigs, KNOT_COMPR_HINT_NONE,
KNOT_PF_CHECKDUP, qdata);
}
return ret;
/*!
* \brief Put NSEC record with corresponding RRSIG into the response.
*/
static int put_nsec_from_node(const zone_node_t *node,
struct query_data *qdata,
knot_pkt_t *resp)
{
return put_nxt_from_node(node, KNOT_RRTYPE_NSEC, qdata, resp);
}
/*!
* \brief Puts NSEC3s covering the non-existent wildcard child of a node
* (and their associated RRSIGs) into the response.
*
* \note This function does not check if DNSSEC is enabled, nor if it is
* requested by the query.
*
* \param zone Zone used for answering.
* \param node Node whose non-existent wildcard child should be covered.
* \param qdata Query data.
* \param resp Response where to add the NSEC3s.
* \brief Put NSEC3 record with corresponding RRSIG into the response.
*/
static int put_nsec3_from_node(const zone_node_t *node,
struct query_data *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.
*
* \retval KNOT_EOK
* \retval NS_ERR_SERVFAIL
* 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_nsec3_no_wildcard_child(const zone_contents_t *zone,
const zone_node_t *node,
struct query_data *qdata,
knot_pkt_t *resp)
static int put_covering_nsec(const zone_contents_t *zone,
const knot_dname_t *name,
struct query_data *qdata,
knot_pkt_t *resp)
{
assert(node != NULL);
assert(resp != NULL);
assert(node->owner != NULL);
const zone_node_t *match = NULL;
const zone_node_t *closest = NULL;
const zone_node_t *prev = NULL;
int ret = 0;
knot_dname_t *wildcard = wildcard_child_name(node->owner);
if (wildcard == NULL) {
ret = KNOT_ERROR; /* servfail */
} else {
ret = put_covering_nsec3(zone, wildcard, qdata, resp);
const zone_node_t *proof = NULL;
/* Directly discard wildcard. */
knot_dname_free(&wildcard, 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 ret;
return put_nsec_from_node(proof, qdata, resp);
}
/*!
* \brief Puts NSEC3s for wildcard answer into the response.
*
* \note This function does not check if DNSSEC is enabled, nor if it is
* requested by the query.
*
* \param zone Zone used for answering.
* \param closest_encloser Closest encloser of \a qname in the zone. In this
* case it is the parent of the source of synthesis.
* \param qname Domain name covered by the wildcard used for answering the
* query.
* \param qdata Query data.
* \param resp Response to put the NSEC3s into.
*
* \retval KNOT_EOK
* \retval NS_ERR_SERVFAIL
* \brief Find NSEC3 covering the given name and put it into the response.
*/
static int put_nsec3_wildcard(const zone_contents_t *zone,
const zone_node_t *closest_encloser,
const knot_dname_t *qname,
static int put_covering_nsec3(const zone_contents_t *zone,
const knot_dname_t *name,
struct query_data *qdata,
knot_pkt_t *resp)
{
assert(closest_encloser != NULL);
assert(qname != NULL);
assert(resp != NULL);
const zone_node_t *prev = NULL;
const zone_node_t *node = NULL;
if (!knot_is_nsec3_enabled(zone)) {
int match = zone_contents_find_nsec3_for_name(zone, name, &node, &prev);
if (match < 0) {
// ignore if missing
return KNOT_EOK;
}
/*
* NSEC3 that covers the "next closer" name.
*/
// create the "next closer" name by appending from qname
knot_dname_t *next_closer =
get_next_closer(closest_encloser->owner, qname);
if (match == ZONE_NAME_FOUND || prev == NULL){
return KNOT_ERROR;
}
if (next_closer == NULL) {
return KNOT_ERROR; /* servfail */
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,
struct query_data *qdata,
knot_pkt_t *resp)
{
knot_dname_t *next_closer = get_next_closer(cpe->owner, qname);
if (!next_closer) {
return KNOT_ENOMEM;
}
int ret = put_covering_nsec3(zone, next_closer, qdata, resp);
/* Duplicate from ns_next_close(), safe to discard. */
knot_dname_free(&next_closer, NULL);
return ret;
}
/*!
* \brief Puts NSECs or NSEC3s for wildcard answer into the response.
* \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,
struct query_data *qdata,
knot_pkt_t *resp)
{
// An NSEC3 RR that matches the closest (provable) encloser.
int ret = put_nsec3_from_node(cpe->nsec3_node, 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.
*
* \note This function first checks if DNSSEC is enabled and requested by the
* query and if the node's owner is a wildcard.
* Add NSEC record proving that no better match on QNAME exists.
*
* \param node Node used for answering.
* \param closest_encloser Closest encloser of \a qname in the zone.
* \param previous Previous node of \a qname in canonical order.
* \param zone Zone used for answering.
* \param qname Actual searched domain name.
* \param qdata Query data.
* \param resp Response where to put the NSECs and NSEC3s.
* \see https://tools.ietf.org/html/rfc4035#section-3.1.3.3
*
* \retval KNOT_EOK
* \retval NS_ERR_SERVFAIL
* \param previous Previous name for QNAME.
* \param qdata Query processing data.
* \param resp Response packet.
*
* \return KNOT_E*
*/
static int put_wildcard_answer(const zone_node_t *node,
const zone_node_t *closest_encloser,
static int put_nsec_wildcard(const zone_node_t *previous,
struct query_data *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,
struct query_data *qdata,
knot_pkt_t *resp)
{
const zone_node_t *cpe = nsec3_encloser(wildcard->parent);
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,
struct query_data *qdata,
knot_pkt_t *resp)
{
int ret = KNOT_EOK;
if (!wildcard_expanded(wildcard, qname)) {
return KNOT_EOK;
}
if (knot_dname_is_wildcard(node->owner)
&& !knot_dname_is_equal(qname, node->owner)) {
if (knot_is_nsec3_enabled(zone)) {
ret = put_nsec3_wildcard(zone, closest_encloser, qname,
qdata, resp);
} else {
ret = put_nsec_wildcard(zone, qname, previous, qdata,
resp);
}
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 Puts NSECs for NXDOMAIN error to the response.
*
* \note This function does not check if DNSSEC is enabled, nor if it is
* requested by the query.
*
* \param qname QNAME which generated the NXDOMAIN error (i.e. not found in the
* zone).
* \param zone Zone used for answering.
* \param previous Previous node to \a qname in the zone. May also be NULL. In
* such case the function finds the previous node in the zone.
* \param closest_encloser Closest encloser of \a qname. Must not be NULL.
* \param qdata Query data.
* \param resp Response where to put the NSECs.
*
* \retval KNOT_EOK
* \retval NS_ERR_SERVFAIL
* \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 qname Source QNAME.
* \param zone Source zone.
* \param previous Previous node to \a qname in the zone.
* \param closest Closest matching parent of \a qname.
* \param qdata Query data.
* \param resp Response packet.
*
* \return KNOT_E*
*/
static int put_nsec_nxdomain(const knot_dname_t *qname,
const zone_contents_t *zone,
const zone_node_t *previous,
const zone_node_t *closest_encloser,
const zone_node_t *closest,
struct query_data *qdata,
knot_pkt_t *resp)
{
knot_rrset_t rrset = { 0 };
knot_rrset_t rrsigs = { 0 };
// check if we have previous; if not, find one using the tree
if (previous == NULL) {
previous = zone_contents_find_previous(zone, qname);
assert(previous != NULL);
while (previous->flags & NODE_FLAGS_NONAUTH) {
previous = previous->prev;
}
}
assert(previous);
assert(closest);
// 1) NSEC proving that there is no node with the searched name
rrset = node_rrset(previous, KNOT_RRTYPE_NSEC);
rrsigs = node_rrset(previous, KNOT_RRTYPE_RRSIG);
if (knot_rrset_empty(&rrset)) {
// no NSEC records
//return NS_ERR_SERVFAIL;
return KNOT_EOK;
}
// An NSEC RR proving that there is no exact match for <SNAME, SCLASS>.
int ret = ns_put_rr(resp, &rrset, &rrsigs, KNOT_COMPR_HINT_NONE, 0, qdata);
previous = nsec_previous(previous);
int ret = put_nsec_from_node(previous, qdata, resp);
if (ret != KNOT_EOK) {
return ret;
}
// 2) NSEC proving that there is no wildcard covering the name
// this is only different from 1) if the wildcard would be
// before 'previous' in canonical order, i.e. we can
// search for previous until we find name lesser than wildcard
assert(closest_encloser != NULL);
// An NSEC RR proving that the zone contains no RRsets that would match
// <SNAME, SCLASS> via wildcard name expansion.