diff options
Diffstat (limited to 'src/knot/modules/onlinesign')
-rw-r--r-- | src/knot/modules/onlinesign/Makefile.inc | 15 | ||||
-rw-r--r-- | src/knot/modules/onlinesign/nsec_next.c | 113 | ||||
-rw-r--r-- | src/knot/modules/onlinesign/nsec_next.h | 29 | ||||
-rw-r--r-- | src/knot/modules/onlinesign/onlinesign.c | 776 | ||||
-rw-r--r-- | src/knot/modules/onlinesign/onlinesign.rst | 152 |
5 files changed, 1085 insertions, 0 deletions
diff --git a/src/knot/modules/onlinesign/Makefile.inc b/src/knot/modules/onlinesign/Makefile.inc new file mode 100644 index 0000000..bfccf3e --- /dev/null +++ b/src/knot/modules/onlinesign/Makefile.inc @@ -0,0 +1,15 @@ +knot_modules_onlinesign_la_SOURCES = knot/modules/onlinesign/onlinesign.c \ + knot/modules/onlinesign/nsec_next.c \ + knot/modules/onlinesign/nsec_next.h +EXTRA_DIST += knot/modules/onlinesign/onlinesign.rst + +if STATIC_MODULE_onlinesign +libknotd_la_SOURCES += $(knot_modules_onlinesign_la_SOURCES) +endif + +if SHARED_MODULE_onlinesign +knot_modules_onlinesign_la_LDFLAGS = $(KNOTD_MOD_LDFLAGS) +knot_modules_onlinesign_la_CPPFLAGS = $(KNOTD_MOD_CPPFLAGS) +knot_modules_onlinesign_la_LIBADD = libcontrib.la +pkglib_LTLIBRARIES += knot/modules/onlinesign.la +endif diff --git a/src/knot/modules/onlinesign/nsec_next.c b/src/knot/modules/onlinesign/nsec_next.c new file mode 100644 index 0000000..1314154 --- /dev/null +++ b/src/knot/modules/onlinesign/nsec_next.c @@ -0,0 +1,113 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + 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 <http://www.gnu.org/licenses/>. +*/ + +#include <assert.h> +#include <stdbool.h> +#include <stdlib.h> + +#include "knot/modules/onlinesign/nsec_next.h" +#include "libknot/libknot.h" + +static bool inc_label(const uint8_t *buffer, uint8_t **label_ptr) +{ + assert(buffer); + assert(label_ptr && *label_ptr); + assert(buffer <= *label_ptr && *label_ptr < buffer + KNOT_DNAME_MAXLEN); + + const uint8_t *label = *label_ptr; + const uint8_t len = *label; + const uint8_t *first = *label_ptr + 1; + const uint8_t *last = *label_ptr + len; + + assert(len <= KNOT_DNAME_MAXLABELLEN); + + // jump over trailing 0xff chars + uint8_t *scan = (uint8_t *)last; + while (scan >= first && *scan == 0xff) { + scan -= 1; + } + + // increase in place + if (scan >= first) { + if (*scan == 'A' - 1) { + *scan = 'Z' + 1; + } else { + *scan += 1; + } + memset(scan + 1, 0x00, last - scan); + return true; + } + + // check name and label boundaries + if (scan - 1 < buffer || len == KNOT_DNAME_MAXLABELLEN) { + return false; + } + + // append a zero byte at the end of the label + scan -= 1; + scan[0] = len + 1; + memmove(scan + 1, first, len); + scan[len + 1] = 0x00; + + *label_ptr = scan; + + return true; +} + +static void strip_label(uint8_t **name_ptr) +{ + assert(name_ptr && *name_ptr); + + uint8_t len = **name_ptr; + *name_ptr += 1 + len; +} + +knot_dname_t *online_nsec_next(const knot_dname_t *dname, const knot_dname_t *apex) +{ + assert(dname); + assert(apex); + + // right aligned copy of the domain name + uint8_t copy[KNOT_DNAME_MAXLEN] = { 0 }; + size_t dname_len = knot_dname_size(dname); + size_t empty_len = sizeof(copy) - dname_len; + uint8_t *pos = copy + empty_len; + memmove(pos, dname, dname_len); + + // add new zero-byte label + if (empty_len >= 2) { + pos -= 2; + pos[0] = 0x01; + pos[1] = 0x00; + return knot_dname_copy(pos, NULL); + } + + // find apex position in the buffer + size_t apex_len = knot_dname_size(apex); + const uint8_t *apex_pos = copy + sizeof(copy) - apex_len; + assert(knot_dname_is_equal(apex, apex_pos)); + + // find first label which can be incremented + while (pos != apex_pos) { + if (inc_label(copy, &pos)) { + return knot_dname_copy(pos, NULL); + } + strip_label(&pos); + } + + // apex completes the chain + return knot_dname_copy(pos, NULL); +} diff --git a/src/knot/modules/onlinesign/nsec_next.h b/src/knot/modules/onlinesign/nsec_next.h new file mode 100644 index 0000000..4d9a6c4 --- /dev/null +++ b/src/knot/modules/onlinesign/nsec_next.h @@ -0,0 +1,29 @@ +/* Copyright (C) 2015 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + 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 <http://www.gnu.org/licenses/>. +*/ + +#pragma once + +#include "libknot/dname.h" + +/*! + * \brief Get the very next possible name in NSEC chain. + * + * \param dname Current dname in the NSEC chain. + * \param apex Zone apex name, used when we reach the end of the chain. + * + * \return Successor of dname in the NSEC chain. + */ +knot_dname_t *online_nsec_next(const knot_dname_t *dname, const knot_dname_t *apex); diff --git a/src/knot/modules/onlinesign/onlinesign.c b/src/knot/modules/onlinesign/onlinesign.c new file mode 100644 index 0000000..8dc9d50 --- /dev/null +++ b/src/knot/modules/onlinesign/onlinesign.c @@ -0,0 +1,776 @@ +/* Copyright (C) 2019 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + 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 <http://www.gnu.org/licenses/>. +*/ + +#include <assert.h> +#include <stddef.h> +#include <string.h> + +#include "contrib/string.h" +#include "libdnssec/error.h" +#include "knot/include/module.h" +#include "knot/modules/onlinesign/nsec_next.h" +// Next dependencies force static module! +#include "knot/dnssec/ds_query.h" +#include "knot/dnssec/key-events.h" +#include "knot/dnssec/policy.h" +#include "knot/dnssec/rrset-sign.h" +#include "knot/dnssec/zone-events.h" +#include "knot/nameserver/query_module.h" +#include "knot/nameserver/process_query.h" + +#define MOD_POLICY "\x06""policy" +#define MOD_NSEC_BITMAP "\x0B""nsec-bitmap" + +int policy_check(knotd_conf_check_args_t *args) +{ + int ret = knotd_conf_check_ref(args); + if (ret != KNOT_EOK && strcmp((const char *)args->data, "default") == 0) { + return KNOT_EOK; + } + + return ret; +} + +int bitmap_check(knotd_conf_check_args_t *args) +{ + uint16_t num; + int ret = knot_rrtype_from_string((const char *)args->data, &num); + if (ret != 0) { + args->err_str = "invalid RR type"; + return KNOT_EINVAL; + } + + return KNOT_EOK; +} + +const yp_item_t online_sign_conf[] = { + { MOD_POLICY, YP_TREF, YP_VREF = { C_POLICY }, YP_FNONE, { policy_check } }, + { MOD_NSEC_BITMAP, YP_TSTR, YP_VNONE, YP_FMULTI, { bitmap_check } }, + { NULL } +}; + +/*! + * We cannot determine the true NSEC bitmap because of dynamic modules which + * can synthesize some types on-the-fly. The base NSEC map will be determined + * from zone content and this list of types. + * + * The types in the NSEC bitmap really don't have to exist. Only the QTYPE + * must not be present. This will make the validation work with resolvers + * performing negative caching. + */ + +static const uint16_t NSEC_FORCE_TYPES[] = { + KNOT_RRTYPE_A, + KNOT_RRTYPE_AAAA, + 0 +}; + +typedef struct { + knot_time_t event_rollover; + knot_time_t event_parent_ds_q; + pthread_mutex_t event_mutex; + pthread_rwlock_t signing_mutex; + + uint16_t *nsec_force_types; + + bool zone_doomed; +} online_sign_ctx_t; + +static bool want_dnssec(knotd_qdata_t *qdata) +{ + return knot_pkt_has_dnssec(qdata->query); +} + +static uint32_t dnskey_ttl(knotd_qdata_t *qdata) +{ + knot_rrset_t soa = knotd_qdata_zone_apex_rrset(qdata, KNOT_RRTYPE_SOA); + return soa.ttl; +} + +static uint32_t nsec_ttl(knotd_qdata_t *qdata) +{ + knot_rrset_t soa = knotd_qdata_zone_apex_rrset(qdata, KNOT_RRTYPE_SOA); + return knot_soa_minimum(soa.rrs.rdata); +} + +/*! + * \brief Add bitmap records synthesized by online-signing. + */ +static void bitmap_add_synth(dnssec_nsec_bitmap_t *map, bool is_apex) +{ + dnssec_nsec_bitmap_add(map, KNOT_RRTYPE_NSEC); + dnssec_nsec_bitmap_add(map, KNOT_RRTYPE_RRSIG); + if (is_apex) { + dnssec_nsec_bitmap_add(map, KNOT_RRTYPE_DNSKEY); + //dnssec_nsec_bitmap_add(map, KNOT_RRTYPE_CDS); + } +} + +/*! + * \brief Add bitmap records present in the zone. + */ +static void bitmap_add_zone(dnssec_nsec_bitmap_t *map, const zone_node_t *node) +{ + if (!node) { + return; + } + + for (int i = 0; i < node->rrset_count; i++) { + dnssec_nsec_bitmap_add(map, node->rrs[i].type); + } +} + +/*! + * \brief Add bitmap records which can be synthesized by other modules. + * + * \param qtype Current QTYPE, will never be added into the map. + */ +static void bitmap_add_forced(dnssec_nsec_bitmap_t *map, uint16_t qtype, + const uint16_t *force_types) +{ + for (int i = 0; force_types[i] > 0; i++) { + if (force_types[i] != qtype) { + dnssec_nsec_bitmap_add(map, force_types[i]); + } + } +} + +/*! + * \brief Add bitmap records from the actual response. + */ +static void bitmap_add_section(dnssec_nsec_bitmap_t *map, const knot_pktsection_t *answer) +{ + for (int i = 0; i < answer->count; i++) { + const knot_rrset_t *rr = knot_pkt_rr(answer, i); + dnssec_nsec_bitmap_add(map, rr->type); + } +} + +/*! + * \brief Synthesize NSEC type bitmap. + * + * - The bitmap will contain types synthesized by this module. + * - For ANY query, the bitmap will contain types from the actual response. + * - For non-ANY query, the bitmap will contain types from zone and forced + * types which can be potentionally synthesized by other query modules. + */ +static dnssec_nsec_bitmap_t *synth_bitmap(knot_pkt_t *pkt, const knotd_qdata_t *qdata, + const uint16_t *force_types) +{ + dnssec_nsec_bitmap_t *map = dnssec_nsec_bitmap_new(); + if (!map) { + return NULL; + } + + uint16_t qtype = knot_pkt_qtype(qdata->query); + bool is_apex = qdata->extra->zone + && qdata->extra->zone->contents + && qdata->extra->node == qdata->extra->zone->contents->apex; + + bitmap_add_synth(map, is_apex); + + if (qtype == KNOT_RRTYPE_ANY) { + const knot_pktsection_t *answer = knot_pkt_section(pkt, KNOT_ANSWER); + bitmap_add_section(map, answer); + } else { + bitmap_add_zone(map, qdata->extra->node); + if (force_types != NULL && !node_rrtype_exists(qdata->extra->node, KNOT_RRTYPE_CNAME)) { + bitmap_add_forced(map, qtype, force_types); + } + } + + return map; +} + +static bool is_deleg(const knot_pkt_t *pkt) +{ + return !knot_wire_get_aa(pkt->wire); +} + +static knot_rrset_t *synth_nsec(knot_pkt_t *pkt, knotd_qdata_t *qdata, knotd_mod_t *mod, + knot_mm_t *mm) +{ + const knot_dname_t *nsec_owner = is_deleg(pkt) ? qdata->extra->encloser->owner : qdata->name; + knot_rrset_t *nsec = knot_rrset_new(nsec_owner, KNOT_RRTYPE_NSEC, + KNOT_CLASS_IN, nsec_ttl(qdata), mm); + if (!nsec) { + return NULL; + } + + knot_dname_t *next = online_nsec_next(nsec_owner, knotd_qdata_zone_name(qdata)); + if (!next) { + knot_rrset_free(nsec, mm); + return NULL; + } + + // If necessary, prepare types to force into NSEC bitmap. + uint16_t *force_types = NULL; + if (!is_deleg(pkt)) { + online_sign_ctx_t *ctx = knotd_mod_ctx(mod); + force_types = ctx->nsec_force_types; + } + + dnssec_nsec_bitmap_t *bitmap = synth_bitmap(pkt, qdata, force_types); + if (!bitmap) { + free(next); + knot_rrset_free(nsec, mm); + return NULL; + } + + size_t size = knot_dname_size(next) + dnssec_nsec_bitmap_size(bitmap); + uint8_t rdata[size]; + + int written = knot_dname_to_wire(rdata, next, size); + dnssec_nsec_bitmap_write(bitmap, rdata + written); + + knot_dname_free(next, NULL); + dnssec_nsec_bitmap_free(bitmap); + + if (knot_rrset_add_rdata(nsec, rdata, size, mm) != KNOT_EOK) { + knot_rrset_free(nsec, mm); + return NULL; + } + + return nsec; +} + +static knot_rrset_t *sign_rrset(const knot_dname_t *owner, + const knot_rrset_t *cover, + knotd_mod_t *mod, + zone_sign_ctx_t *sign_ctx, + knot_mm_t *mm) +{ + // copy of RR set with replaced owner name + + knot_rrset_t *copy = knot_rrset_new(owner, cover->type, cover->rclass, + cover->ttl, NULL); + if (!copy) { + return NULL; + } + + if (knot_rdataset_copy(©->rrs, &cover->rrs, NULL) != KNOT_EOK) { + knot_rrset_free(copy, NULL); + return NULL; + } + + // resulting RRSIG + + knot_rrset_t *rrsig = knot_rrset_new(owner, KNOT_RRTYPE_RRSIG, copy->rclass, + copy->ttl, mm); + if (!rrsig) { + knot_rrset_free(copy, NULL); + return NULL; + } + + online_sign_ctx_t *ctx = knotd_mod_ctx(mod); + pthread_rwlock_rdlock(&ctx->signing_mutex); + int ret = knot_sign_rrset2(rrsig, copy, sign_ctx, mm); + pthread_rwlock_unlock(&ctx->signing_mutex); + if (ret != KNOT_EOK) { + knot_rrset_free(copy, NULL); + knot_rrset_free(rrsig, mm); + return NULL; + } + + knot_rrset_free(copy, NULL); + + return rrsig; +} + +static glue_t *find_glue_for(const knot_rrset_t *rr, const knot_pkt_t *pkt) +{ + for (int i = KNOT_ANSWER; i <= KNOT_AUTHORITY; i++) { + const knot_pktsection_t *section = knot_pkt_section(pkt, i); + for (int j = 0; j < section->count; j++) { + const knot_rrset_t *attempt = knot_pkt_rr(section, j); + const additional_t *a = attempt->additional; + for (int k = 0; a != NULL && k < a->count; k++) { + // no need for knot_dname_cmp because the pointers are assigned + if (a->glues[k].node->owner == rr->owner) { + return &a->glues[k]; + } + } + } + } + return NULL; +} + +static bool shall_sign_rr(const knot_rrset_t *rr, const knot_pkt_t *pkt) +{ + if (pkt->current == KNOT_ADDITIONAL) { + glue_t *g = find_glue_for(rr, pkt); + assert(g); // finds actually the node which is rr in + return !(g->node->flags & NODE_FLAGS_NONAUTH); + } else { + return !is_deleg(pkt) || rr->type == KNOT_RRTYPE_NSEC; + } +} + +static knotd_in_state_t sign_section(knotd_in_state_t state, knot_pkt_t *pkt, + knotd_qdata_t *qdata, knotd_mod_t *mod) +{ + if (!want_dnssec(qdata)) { + return state; + } + + const knot_pktsection_t *section = knot_pkt_section(pkt, pkt->current); + assert(section); + + zone_sign_ctx_t *sign_ctx = zone_sign_ctx(mod->keyset, mod->dnssec); + if (sign_ctx == NULL) { + return KNOTD_IN_STATE_ERROR; + } + + uint16_t count_unsigned = section->count; + for (int i = 0; i < count_unsigned; i++) { + const knot_rrset_t *rr = knot_pkt_rr(section, i); + if (!shall_sign_rr(rr, pkt)) { + continue; + } + + uint16_t rr_pos = knot_pkt_rr_offset(section, i); + + uint8_t owner[KNOT_DNAME_MAXLEN] = { 0 }; + knot_dname_unpack(owner, pkt->wire + rr_pos, sizeof(owner), pkt->wire); + knot_dname_to_lower(owner); + + knot_rrset_t *rrsig = sign_rrset(owner, rr, mod, sign_ctx, &pkt->mm); + if (!rrsig) { + state = KNOTD_IN_STATE_ERROR; + break; + } + + int r = knot_pkt_put(pkt, KNOT_COMPR_HINT_NONE, rrsig, KNOT_PF_FREE); + if (r != KNOT_EOK) { + knot_rrset_free(rrsig, &pkt->mm); + state = KNOTD_IN_STATE_ERROR; + break; + } + } + + zone_sign_ctx_free(sign_ctx); + + return state; +} + +static knotd_in_state_t synth_authority(knotd_in_state_t state, knot_pkt_t *pkt, + knotd_qdata_t *qdata, knotd_mod_t *mod) +{ + if (state == KNOTD_IN_STATE_HIT) { + return state; + } + + // synthesise NSEC + + if (want_dnssec(qdata)) { + knot_rrset_t *nsec = synth_nsec(pkt, qdata, mod, &pkt->mm); + int r = knot_pkt_put(pkt, KNOT_COMPR_HINT_NONE, nsec, KNOT_PF_FREE); + if (r != DNSSEC_EOK) { + knot_rrset_free(nsec, &pkt->mm); + return KNOTD_IN_STATE_ERROR; + } + } + + // promote NXDOMAIN to NODATA + + if (state == KNOTD_IN_STATE_MISS) { + //! \todo Override RCODE set in solver_authority. Review. + qdata->rcode = KNOT_RCODE_NOERROR; + return KNOTD_IN_STATE_NODATA; + } + + return state; +} + +static knot_rrset_t *synth_dnskey(knotd_qdata_t *qdata, knotd_mod_t *mod, + knot_mm_t *mm) +{ + knot_rrset_t *dnskey = knot_rrset_new(knotd_qdata_zone_name(qdata), + KNOT_RRTYPE_DNSKEY, KNOT_CLASS_IN, + dnskey_ttl(qdata), mm); + if (!dnskey) { + return 0; + } + + dnssec_binary_t rdata = { 0 }; + online_sign_ctx_t *ctx = knotd_mod_ctx(mod); + pthread_rwlock_rdlock(&ctx->signing_mutex); + for (size_t i = 0; i < mod->keyset->count; i++) { + if (!mod->keyset->keys[i].is_public) { + continue; + } + + dnssec_key_get_rdata(mod->keyset->keys[i].key, &rdata); + assert(rdata.size > 0 && rdata.data); + + int r = knot_rrset_add_rdata(dnskey, rdata.data, rdata.size, mm); + if (r != KNOT_EOK) { + knot_rrset_free(dnskey, mm); + pthread_rwlock_unlock(&ctx->signing_mutex); + return NULL; + } + } + + pthread_rwlock_unlock(&ctx->signing_mutex); + return dnskey; +} + +/* copied from the middle of zone-sign.c */ +static zone_key_t *ksk_for_cds(knotd_mod_t *mod) +{ + zone_key_t *res = NULL; + unsigned crp = mod->dnssec->policy->child_records_publish; + int kfc_prio = (crp == CHILD_RECORDS_ALWAYS ? + 0 : (crp == CHILD_RECORDS_ROLLOVER ? 1 : 2)); + for (int i = 0; i < mod->keyset->count; i++) { + zone_key_t *key = &mod->keyset->keys[i]; + if (key->is_ksk && key->cds_priority > kfc_prio) { + res = key; + kfc_prio = key->cds_priority; + } + } + return res; +} + +static knot_rrset_t *synth_cdnskey(knotd_qdata_t *qdata, knotd_mod_t *mod, + knot_mm_t *mm) +{ + knot_rrset_t *dnskey = knot_rrset_new(knotd_qdata_zone_name(qdata), + KNOT_RRTYPE_CDNSKEY, KNOT_CLASS_IN, + 0, mm); + if (dnskey == NULL) { + return 0; + } + + dnssec_binary_t rdata = { 0 }; + online_sign_ctx_t *ctx = knotd_mod_ctx(mod); + pthread_rwlock_rdlock(&ctx->signing_mutex); + zone_key_t *key = ksk_for_cds(mod); + if (key == NULL) { + pthread_rwlock_unlock(&ctx->signing_mutex); + knot_rrset_free(dnskey, mm); + return NULL; + } + dnssec_key_get_rdata(key->key, &rdata); + assert(rdata.size > 0 && rdata.data); + + int ret = knot_rrset_add_rdata(dnskey, rdata.data, rdata.size, mm); + pthread_rwlock_unlock(&ctx->signing_mutex); + if (ret != KNOT_EOK) { + knot_rrset_free(dnskey, mm); + return NULL; + } + + return dnskey; +} + +static knot_rrset_t *synth_cds(knotd_qdata_t *qdata, knotd_mod_t *mod, + knot_mm_t *mm) +{ + knot_rrset_t *ds = knot_rrset_new(knotd_qdata_zone_name(qdata), + KNOT_RRTYPE_CDS, KNOT_CLASS_IN, + 0, mm); + if (ds == NULL) { + return 0; + } + + dnssec_binary_t rdata = { 0 }; + online_sign_ctx_t *ctx = knotd_mod_ctx(mod); + pthread_rwlock_rdlock(&ctx->signing_mutex); + zone_key_t *key = ksk_for_cds(mod); + if (key == NULL) { + pthread_rwlock_unlock(&ctx->signing_mutex); + knot_rrset_free(ds, mm); + return NULL; + } + zone_key_calculate_ds(key, &rdata); + assert(rdata.size > 0 && rdata.data); + + int ret = knot_rrset_add_rdata(ds, rdata.data, rdata.size, mm); + pthread_rwlock_unlock(&ctx->signing_mutex); + if (ret != KNOT_EOK) { + knot_rrset_free(ds, mm); + return NULL; + } + + return ds; +} + +static bool qtype_match(knotd_qdata_t *qdata, uint16_t type) +{ + uint16_t qtype = knot_pkt_qtype(qdata->query); + return (qtype == KNOT_RRTYPE_ANY || qtype == type); +} + +static bool is_apex_query(knotd_qdata_t *qdata) +{ + return knot_dname_is_equal(qdata->name, knotd_qdata_zone_name(qdata)); +} + +static knotd_in_state_t pre_routine(knotd_in_state_t state, knot_pkt_t *pkt, + knotd_qdata_t *qdata, knotd_mod_t *mod) +{ + online_sign_ctx_t *ctx = knotd_mod_ctx(mod); + zone_sign_reschedule_t resch = { .allow_rollover = true }; + + (void)pkt, (void)qdata; + + pthread_mutex_lock(&ctx->event_mutex); + if (ctx->zone_doomed) { + pthread_mutex_unlock(&ctx->event_mutex); + return KNOTD_IN_STATE_ERROR; + } + mod->dnssec->now = time(NULL); + int ret = KNOT_ESEMCHECK; + if (knot_time_cmp(ctx->event_parent_ds_q, mod->dnssec->now) <= 0) { + pthread_rwlock_rdlock(&ctx->signing_mutex); + ret = knot_parent_ds_query(mod->dnssec, mod->keyset, 1000); + pthread_rwlock_unlock(&ctx->signing_mutex); + if (ret != KNOT_EOK && mod->dnssec->policy->ksk_sbm_check_interval > 0) { + ctx->event_parent_ds_q = mod->dnssec->now + mod->dnssec->policy->ksk_sbm_check_interval; + } else { + ctx->event_parent_ds_q = 0; + } + } + if (ret == KNOT_EOK || knot_time_cmp(ctx->event_rollover, mod->dnssec->now) <= 0) { + update_policy_from_zone(mod->dnssec->policy, qdata->extra->zone->contents); + ret = knot_dnssec_key_rollover(mod->dnssec, &resch); + } + if (ret == KNOT_EOK) { + if (resch.plan_ds_query && mod->dnssec->policy->ksk_sbm_check_interval > 0) { + ctx->event_parent_ds_q = mod->dnssec->now + mod->dnssec->policy->ksk_sbm_check_interval; + } else { + ctx->event_parent_ds_q = 0; + } + + ctx->event_rollover = resch.next_rollover; + + pthread_rwlock_wrlock(&ctx->signing_mutex); + knotd_mod_dnssec_unload_keyset(mod); + ret = knotd_mod_dnssec_load_keyset(mod, true); + if (ret != KNOT_EOK) { + ctx->zone_doomed = true; + state = KNOTD_IN_STATE_ERROR; + } else { + ctx->event_rollover = knot_time_min(ctx->event_rollover, knot_get_next_zone_key_event(mod->keyset)); + } + pthread_rwlock_unlock(&ctx->signing_mutex); + } + pthread_mutex_unlock(&ctx->event_mutex); + + return state; +} + +static knotd_in_state_t synth_answer(knotd_in_state_t state, knot_pkt_t *pkt, + knotd_qdata_t *qdata, knotd_mod_t *mod) +{ + // disallowed queries + + if (knot_pkt_qtype(pkt) == KNOT_RRTYPE_RRSIG) { + qdata->rcode = KNOT_RCODE_REFUSED; + return KNOTD_IN_STATE_ERROR; + } + + // synthesized DNSSEC answers + + if (qtype_match(qdata, KNOT_RRTYPE_DNSKEY) && is_apex_query(qdata)) { + knot_rrset_t *dnskey = synth_dnskey(qdata, mod, &pkt->mm); + if (!dnskey) { + return KNOTD_IN_STATE_ERROR; + } + + int r = knot_pkt_put(pkt, KNOT_COMPR_HINT_QNAME, dnskey, KNOT_PF_FREE); + if (r != DNSSEC_EOK) { + knot_rrset_free(dnskey, &pkt->mm); + return KNOTD_IN_STATE_ERROR; + } + state = KNOTD_IN_STATE_HIT; + } + + if (qtype_match(qdata, KNOT_RRTYPE_CDNSKEY) && is_apex_query(qdata)) { + knot_rrset_t *dnskey = synth_cdnskey(qdata, mod, &pkt->mm); + if (!dnskey) { + return KNOTD_IN_STATE_ERROR; + } + + int r = knot_pkt_put(pkt, KNOT_COMPR_HINT_QNAME, dnskey, KNOT_PF_FREE); + if (r != DNSSEC_EOK) { + knot_rrset_free(dnskey, &pkt->mm); + return KNOTD_IN_STATE_ERROR; + } + state = KNOTD_IN_STATE_HIT; + } + + if (qtype_match(qdata, KNOT_RRTYPE_CDS) && is_apex_query(qdata)) { + knot_rrset_t *ds = synth_cds(qdata, mod, &pkt->mm); + if (!ds) { + return KNOTD_IN_STATE_ERROR; + } + + int r = knot_pkt_put(pkt, KNOT_COMPR_HINT_QNAME, ds, KNOT_PF_FREE); + if (r != DNSSEC_EOK) { + knot_rrset_free(ds, &pkt->mm); + return KNOTD_IN_STATE_ERROR; + } + state = KNOTD_IN_STATE_HIT; + } + + if (qtype_match(qdata, KNOT_RRTYPE_NSEC)) { + knot_rrset_t *nsec = synth_nsec(pkt, qdata, mod, &pkt->mm); + if (!nsec) { + return KNOTD_IN_STATE_ERROR; + } + + int r = knot_pkt_put(pkt, KNOT_COMPR_HINT_QNAME, nsec, KNOT_PF_FREE); + if (r != DNSSEC_EOK) { + knot_rrset_free(nsec, &pkt->mm); + return KNOTD_IN_STATE_ERROR; + } + + state = KNOTD_IN_STATE_HIT; + } + + return state; +} + +static void online_sign_ctx_free(online_sign_ctx_t *ctx) +{ + pthread_mutex_destroy(&ctx->event_mutex); + pthread_rwlock_destroy(&ctx->signing_mutex); + + free(ctx->nsec_force_types); + free(ctx); +} + +static int online_sign_ctx_new(online_sign_ctx_t **ctx_ptr, knotd_mod_t *mod) +{ + online_sign_ctx_t *ctx = calloc(1, sizeof(*ctx)); + if (!ctx) { + return KNOT_ENOMEM; + } + + int ret = knotd_mod_dnssec_init(mod); + if (ret != KNOT_EOK) { + free(ctx); + return ret; + } + + // Force Singe-Type signing scheme. This is only important for compatibility with older versions. + mod->dnssec->policy->singe_type_signing = true; + + zone_sign_reschedule_t resch = { + .allow_rollover = true + }; + ret = knot_dnssec_key_rollover(mod->dnssec, &resch); + if (ret != KNOT_EOK) { + free(ctx); + return ret; + } + + if (resch.plan_ds_query) { + ctx->event_parent_ds_q = time(NULL); + } + ctx->event_rollover = resch.next_rollover; + + ret = knotd_mod_dnssec_load_keyset(mod, true); + if (ret != KNOT_EOK) { + free(ctx); + return ret; + } + + ctx->event_rollover = knot_time_min(ctx->event_rollover, knot_get_next_zone_key_event(mod->keyset)); + + pthread_mutex_init(&ctx->event_mutex, NULL); + pthread_rwlock_init(&ctx->signing_mutex, NULL); + + *ctx_ptr = ctx; + + return KNOT_EOK; +} + +int load_nsec_bitmap(online_sign_ctx_t *ctx, knotd_conf_t *conf) +{ + int count = (conf->count > 0) ? conf->count : sizeof(NSEC_FORCE_TYPES) / sizeof(uint16_t); + ctx->nsec_force_types = calloc(count + 1, sizeof(uint16_t)); + if (ctx->nsec_force_types == NULL) { + return KNOT_ENOMEM; + } + + if (conf->count == 0) { + // Use the default list. + for (int i = 0; NSEC_FORCE_TYPES[i] > 0; i++) { + ctx->nsec_force_types[i] = NSEC_FORCE_TYPES[i]; + } + } else { + for (int i = 0; i < conf->count; i++) { + int ret = knot_rrtype_from_string(conf->multi[i].string, + &ctx->nsec_force_types[i]); + if (ret != 0) { + return KNOT_EINVAL; + } + } + } + + return KNOT_EOK; +} + +int online_sign_load(knotd_mod_t *mod) +{ + knotd_conf_t conf = knotd_conf_zone(mod, C_DNSSEC_SIGNING, + knotd_mod_zone(mod)); + if (conf.single.boolean) { + knotd_mod_log(mod, LOG_ERR, "incompatible with automatic signing"); + return KNOT_ENOTSUP; + } + + online_sign_ctx_t *ctx = NULL; + int ret = online_sign_ctx_new(&ctx, mod); + if (ret != KNOT_EOK) { + knotd_mod_log(mod, LOG_ERR, "failed to initialize signing key (%s)", + knot_strerror(ret)); + return KNOT_ERROR; + } + + conf = knotd_conf_mod(mod, MOD_NSEC_BITMAP); + ret = load_nsec_bitmap(ctx, &conf); + knotd_conf_free(&conf); + if (ret != KNOT_EOK) { + online_sign_ctx_free(ctx); + return ret; + } + + knotd_mod_ctx_set(mod, ctx); + + knotd_mod_in_hook(mod, KNOTD_STAGE_ANSWER, pre_routine); + + knotd_mod_in_hook(mod, KNOTD_STAGE_ANSWER, synth_answer); + knotd_mod_in_hook(mod, KNOTD_STAGE_ANSWER, sign_section); + + knotd_mod_in_hook(mod, KNOTD_STAGE_AUTHORITY, synth_authority); + knotd_mod_in_hook(mod, KNOTD_STAGE_AUTHORITY, sign_section); + + knotd_mod_in_hook(mod, KNOTD_STAGE_ADDITIONAL, sign_section); + + return KNOT_EOK; +} + +void online_sign_unload(knotd_mod_t *mod) +{ + online_sign_ctx_free(knotd_mod_ctx(mod)); +} + +KNOTD_MOD_API(onlinesign, KNOTD_MOD_FLAG_SCOPE_ZONE | KNOTD_MOD_FLAG_OPT_CONF, + online_sign_load, online_sign_unload, online_sign_conf, NULL); diff --git a/src/knot/modules/onlinesign/onlinesign.rst b/src/knot/modules/onlinesign/onlinesign.rst new file mode 100644 index 0000000..5b34186 --- /dev/null +++ b/src/knot/modules/onlinesign/onlinesign.rst @@ -0,0 +1,152 @@ +.. _mod-onlinesign: + +``onlinesign`` — Online DNSSEC signing +====================================== + +The module provides online DNSSEC signing. Instead of pre-computing the zone +signatures when the zone is loaded into the server or instead of loading an +externally signed zone, the signatures are computed on-the-fly during +answering. + +The main purpose of the module is to enable authenticated responses with +zones which use other dynamic module (e.g., automatic reverse record +synthesis) because these zones cannot be pre-signed. However, it can be also +used as a simple signing solution for zones with low traffic and also as +a protection against zone content enumeration (zone walking). + +In order to minimize the number of computed signatures per query, the module +produces a bit different responses from the responses that would be sent if +the zone was pre-signed. Still, the responses should be perfectly valid for +a DNSSEC validating resolver. + +.. rubric:: Differences from statically signed zones: + +* The NSEC records are constructed as Minimally Covering NSEC Records + (:rfc:`7129#appendix-A`). Therefore the generated domain names cover + the complete domain name space in the zone's authority. + +* NXDOMAIN responses are promoted to NODATA responses. The module proves + that the query type does not exist rather than that the domain name does not + exist. + +* Domain names matching a wildcard are expanded. The module pretends and proves + that the domain name exists rather than proving a presence of the wildcard. + +.. rubric:: Records synthesized by the module: + +* DNSKEY record is synthesized in the zone apex and includes public key + material for the active signing key. + +* NSEC records are synthesized as needed. + +* RRSIG records are synthesized for authoritative content of the zone. + +* CDNSKEY and CDS records are generated as usual to publish valid Secure Entry Point. + +.. rubric:: Known issues: + +* The `knotc zone-ksk-submitted` command does not work well and is discouraged. + The module must be reloaded afterwards. + +.. rubric:: Limitations: + +* Online-sign module always enforces Single-Type Signing scheme. + +* After any change to KASP DB, the module must be reloaded to apply the changed keys. + +* The NSEC records may differ for one domain name if queried for different + types. This is an implementation shortcoming as the dynamic modules + cooperate loosely. Possible synthesis of a type by other module cannot + be predicted. This dissimilarity should not affect response validation, + even with validators performing `aggressive negative caching + <https://datatracker.ietf.org/doc/draft-fujiwara-dnsop-nsec-aggressiveuse/>`_. + +.. rubric:: Recommendations: + +* Configure the module with an explicit signing policy which has the + :ref:`policy_rrsig-lifetime` value in the order of hours. + +Example +------- + +* Enable the module in the zone configuration with the default signing policy:: + + zone: + - domain: example.com + module: mod-onlinesign + + Or with an explicit signing policy:: + + policy: + - id: rsa + algorithm: RSASHA256 + zsk-size: 2048 + rrsig-lifetime: 25h + + mod-onlinesign: + - id: explicit + policy: rsa + + zone: + - domain: example.com + module: mod-onlinesign/explicit + + Or use manual policy in an analogous manner, see + :ref:`Manual key management<dnssec-manual-key-management>`. + +* Make sure the zone is not signed and also that the automatic signing is + disabled. All is set, you are good to go. Reload (or start) the server: + + .. code-block:: console + + $ knotc reload + +The following example stacks the online signing with reverse record synthesis +module:: + + mod-synthrecord: + - id: lan-forward + type: forward + prefix: ip- + ttl: 1200 + network: 192.168.100.0/24 + + zone: + - domain: corp.example.net + module: [mod-synthrecord/lan-forward, mod-onlinesign] + +Module reference +---------------- + +:: + + mod-onlinesign: + - id: STR + policy: STR + nsec-bitmap: STR ... + +.. _mod-onlinesign_id: + +id +.. + +A module identifier. + +.. _mod-onlinesign_policy: + +policy +...... + +A :ref:`reference<policy_id>` to DNSSEC signing policy. A special *default* +value can be used for the default policy setting. + +.. _mod-onlinesign_nsec-bitmap: + +nsec-bitmap +........... + +A list of Resource Record types included in an NSEC bitmap generated by the module. +This option should reflect zone contents or synthesized responses by modules, +such as :ref:`synthrecord<mod-synthrecord>` and :ref:`GeoIP<mod-geoip>`. + +*Default:* [A, AAAA] |