summaryrefslogtreecommitdiffstats
path: root/src/knot/modules/onlinesign
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 15:24:08 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 15:24:08 +0000
commitf449f278dd3c70e479a035f50a9bb817a9b433ba (patch)
tree8ca2bfb785dda9bb4d573acdf9b42aea9cd51383 /src/knot/modules/onlinesign
parentInitial commit. (diff)
downloadknot-f449f278dd3c70e479a035f50a9bb817a9b433ba.tar.xz
knot-f449f278dd3c70e479a035f50a9bb817a9b433ba.zip
Adding upstream version 3.2.6.upstream/3.2.6upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/knot/modules/onlinesign')
-rw-r--r--src/knot/modules/onlinesign/Makefile.inc15
-rw-r--r--src/knot/modules/onlinesign/nsec_next.c113
-rw-r--r--src/knot/modules/onlinesign/nsec_next.h29
-rw-r--r--src/knot/modules/onlinesign/onlinesign.c736
-rw-r--r--src/knot/modules/onlinesign/onlinesign.rst158
5 files changed, 1051 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..e7289fb
--- /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_LIBS)
+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..2205f6b
--- /dev/null
+++ b/src/knot/modules/onlinesign/nsec_next.c
@@ -0,0 +1,113 @@
+/* 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 <https://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
+ knot_dname_storage_t copy = { 0 };
+ const size_t dname_len = knot_dname_size(dname);
+ const size_t empty_len = sizeof(copy) - dname_len;
+ memmove(copy + empty_len, dname, dname_len);
+
+ // add new zero-byte label
+ if (empty_len >= 2) {
+ uint8_t *pos = copy + empty_len - 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
+ uint8_t *pos = copy + empty_len;
+ 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..428b993
--- /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 <https://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..56b1c03
--- /dev/null
+++ b/src/knot/modules/onlinesign/onlinesign.c
@@ -0,0 +1,736 @@
+/* Copyright (C) 2022 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 <https://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/dnssec/zone-sign.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 Synthesize NSEC type bitmap.
+ *
+ * - The bitmap will contain types synthesized by this module.
+ * - The bitmap will contain types from zone and forced
+ * types which can be potentially synthesized by other query modules.
+ */
+static dnssec_nsec_bitmap_t *synth_bitmap(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->contents != NULL &&
+ qdata->extra->node == qdata->extra->contents->apex);
+
+ bitmap_add_synth(map, is_apex);
+
+ 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(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(&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, knotd_qdata_t *qdata)
+{
+ if (pkt->current == KNOT_ADDITIONAL) {
+ glue_t *g = find_glue_for(rr, pkt);
+ assert(g); // finds actually the node which is rr in
+ const zone_node_t *gn = glue_node(g, qdata->extra->node);
+ return !(gn->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, qdata)) {
+ continue;
+ }
+
+ uint16_t rr_pos = knot_pkt_rr_offset(section, i);
+
+ knot_dname_storage_t owner;
+ 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 (want_dnssec(qdata) && 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;
+}
+
+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);
+ keyptr_dynarray_t kcdnskeys = knot_zone_sign_get_cdnskeys(mod->dnssec, mod->keyset);
+ knot_dynarray_foreach(keyptr, zone_key_t *, ksk_for_cdnskey, kcdnskeys) {
+ dnssec_key_get_rdata((*ksk_for_cdnskey)->key, &rdata);
+ assert(rdata.size > 0 && rdata.data);
+ (void)knot_rrset_add_rdata(dnskey, rdata.data, rdata.size, mm);
+ }
+ pthread_rwlock_unlock(&ctx->signing_mutex);
+
+ 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);
+ keyptr_dynarray_t kcdnskeys = knot_zone_sign_get_cdnskeys(mod->dnssec, mod->keyset);
+ knot_dynarray_foreach(keyptr, zone_key_t *, ksk_for_cds, kcdnskeys) {
+ zone_key_calculate_ds(*ksk_for_cds, mod->dnssec->policy->cds_dt, &rdata);
+ assert(rdata.size > 0 && rdata.data);
+ (void)knot_rrset_add_rdata(ds, rdata.data, rdata.size, mm);
+ }
+ pthread_rwlock_unlock(&ctx->signing_mutex);
+
+ return ds;
+}
+
+static bool qtype_match(knotd_qdata_t *qdata, uint16_t type)
+{
+ uint16_t qtype = knot_pkt_qtype(qdata->query);
+ return (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 = { 0 };
+
+ (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(conf(), mod->dnssec, 1000);
+ pthread_rwlock_unlock(&ctx->signing_mutex);
+ if (ret != KNOT_EOK && ret != KNOT_NO_READY_KEY && 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->contents);
+ ret = knot_dnssec_key_rollover(mod->dnssec, KEY_ROLL_ALLOW_KSK_ROLL | KEY_ROLL_ALLOW_ZSK_ROLL, &resch);
+ if (ret != KNOT_EOK) {
+ ctx->event_rollover = knot_dnssec_failover_delay(mod->dnssec);
+ }
+ }
+ if (ret == KNOT_EOK) {
+ if (resch.plan_ds_check && 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;
+ }
+
+ // Historically, the default scheme is Single-Type signing.
+ if (mod->dnssec->policy->sts_default) {
+ mod->dnssec->policy->single_type_signing = true;
+ }
+
+ zone_sign_reschedule_t resch = { 0 };
+ ret = knot_dnssec_key_rollover(mod->dnssec, KEY_ROLL_ALLOW_KSK_ROLL | KEY_ROLL_ALLOW_ZSK_ROLL, &resch);
+ if (ret != KNOT_EOK) {
+ free(ctx);
+ return ret;
+ }
+
+ if (resch.plan_ds_check) {
+ 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;
+ }
+
+ if (mod->dnssec->policy->offline_ksk) {
+ knotd_mod_log(mod, LOG_ERR, "incompatible with offline KSK mode");
+ online_sign_ctx_free(ctx);
+ return KNOT_ENOTSUP;
+ }
+
+ 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..c1859e2
--- /dev/null
+++ b/src/knot/modules/onlinesign/onlinesign.rst
@@ -0,0 +1,158 @@
+.. _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:: Limitations:
+
+* Due to limited interaction between the server and the module,
+ after any change to KASP DB (including `knotc zone-ksk-submitted` command)
+ or when a scheduled DNSSEC event shall be processed (e.g. transition to next
+ DNSKEY rollover state) the server must be reloaded or queried to the zone
+ (with the DO bit set) to apply the change or to trigger the event. For optimal
+ operation, the recommended query frequency is at least ones per second for
+ each zone configured.
+
+* 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 (:rfc:`8198`).
+
+* The module isn't compatible with the Offline KSK mode yet.
+
+.. rubric:: Recommendations:
+
+* Configure the module with an explicit signing policy which has the
+ :ref:`policy_rrsig-lifetime` value in the order of hours.
+
+* Note that :ref:`policy_single-type-signing` should be set explicitly to
+ avoid fallback to backward-compatible default.
+
+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
+ ksk-size: 2048
+ rrsig-lifetime: 25h
+ rrsig-refresh: 20h
+
+ 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: policy_id
+ 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.
+
+*Default:* an imaginary policy with all default values
+
+.. _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]``