diff options
Diffstat (limited to 'src/utils/keymgr/offline_ksk.c')
-rw-r--r-- | src/utils/keymgr/offline_ksk.c | 549 |
1 files changed, 549 insertions, 0 deletions
diff --git a/src/utils/keymgr/offline_ksk.c b/src/utils/keymgr/offline_ksk.c new file mode 100644 index 0000000..1f1110e --- /dev/null +++ b/src/utils/keymgr/offline_ksk.c @@ -0,0 +1,549 @@ +/* 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 <string.h> +#include <stdio.h> +#include <time.h> + +#include "utils/keymgr/offline_ksk.h" +#include "knot/dnssec/kasp/policy.h" +#include "knot/dnssec/key-events.h" +#include "knot/dnssec/key_records.h" +#include "knot/dnssec/rrset-sign.h" +#include "knot/dnssec/zone-events.h" +#include "knot/dnssec/zone-keys.h" +#include "knot/dnssec/zone-sign.h" +#include "libzscanner/scanner.h" +#include "utils/common/msg.h" +#include "utils/keymgr/functions.h" + +#define KSR_SKR_VER "1.0" + +static int pregenerate_once(kdnssec_ctx_t *ctx, knot_time_t *next) +{ + zone_sign_reschedule_t resch = { 0 }; + + // generate ZSKs + int ret = knot_dnssec_key_rollover(ctx, KEY_ROLL_ALLOW_ZSK_ROLL, &resch); + if (ret != KNOT_EOK) { + ERR2("key rollover failed"); + return ret; + } + // we don't need to do anything explicitly with the generated ZSKs + // they're simply stored in KASP db + + *next = resch.next_rollover; + return KNOT_EOK; +} + +// please free *_dnskey and keyset even if returned error +static int load_dnskey_rrset(kdnssec_ctx_t *ctx, knot_rrset_t **_dnskey, zone_keyset_t *keyset) +{ + // prepare the DNSKEY rrset to be signed + knot_rrset_t *dnskey = knot_rrset_new(ctx->zone->dname, KNOT_RRTYPE_DNSKEY, + KNOT_CLASS_IN, ctx->policy->dnskey_ttl, NULL); + if (dnskey == NULL) { + return KNOT_ENOMEM; + } + *_dnskey = dnskey; + + int ret = load_zone_keys(ctx, keyset, false); + if (ret != KNOT_EOK) { + ERR2("failed to load keys"); + return ret; + } + + for (int i = 0; i < keyset->count; i++) { + zone_key_t *key = &keyset->keys[i]; + if (key->is_public) { + ret = rrset_add_zone_key(dnskey, key); + if (ret != KNOT_EOK) { + ERR2("failed to add zone key"); + return ret; + } + } + } + + return KNOT_EOK; +} + +int keymgr_pregenerate_zsks(kdnssec_ctx_t *ctx, char *arg_from, char *arg_to) +{ + knot_time_t from = 0, to; + int ret = parse_timestamp(arg_to, &to); + if (ret != KNOT_EOK) { + return ret; + } + if (arg_from != NULL) { + ret = parse_timestamp(arg_from, &from); + if (ret != KNOT_EOK) { + return ret; + } + } + + knot_time_t next = (from == 0 ? ctx->now : from); + ret = KNOT_EOK; + + ctx->keep_deleted_keys = true; + ctx->policy->manual = false; + + if (ctx->policy->dnskey_ttl == UINT32_MAX || + ctx->policy->zone_maximal_ttl == UINT32_MAX) { + ERR2("dnskey-ttl or zone-max-ttl not configured"); + return KNOT_ESEMCHECK; + } + + while (ret == KNOT_EOK && knot_time_cmp(next, to) <= 0) { + ctx->now = next; + ret = pregenerate_once(ctx, &next); + } + + return ret; +} + +static int dump_rrset_to_buf(const knot_rrset_t *rrset, char **buf, size_t *buf_size) +{ + if (*buf == NULL) { + *buf = malloc(*buf_size); + if (*buf == NULL) { + return KNOT_ENOMEM; + } + } + + knot_dump_style_t style = { + .wrap = true, + .show_ttl = true, + .verbose = true, + .original_ttl = true, + .human_timestamp = true + }; + return knot_rrset_txt_dump(rrset, buf, buf_size, &style); +} + +static void print_header(const char *of_what, knot_time_t timestamp, const char *contents) +{ + char date[64] = { 0 }; + (void)knot_time_print(TIME_PRINT_ISO8601, timestamp, date, sizeof(date)); + printf(";; %s %"PRIu64" (%s) =========\n%s", of_what, + timestamp, date, contents); +} + +int keymgr_print_offline_records(kdnssec_ctx_t *ctx, char *arg_from, char *arg_to) +{ + knot_time_t from = 0, to = 0; + if (arg_from != NULL) { + int ret = parse_timestamp(arg_from, &from); + if (ret != KNOT_EOK) { + return ret; + } + } + if (arg_to != NULL) { + int ret = parse_timestamp(arg_to, &to); + if (ret != KNOT_EOK) { + return ret; + } + } + + bool empty = true; + char *buf = NULL; + size_t buf_size = 512; + while (true) { + if (arg_to != NULL && knot_time_cmp(from, to) > 0) { + break; + } + knot_time_t next; + key_records_t r = { { 0 } }; + int ret = kasp_db_load_offline_records(ctx->kasp_db, ctx->zone->dname, + &from, &next, &r); + if (ret == KNOT_ENOENT) { + break; + } else if (ret != KNOT_EOK) { + free(buf); + return ret; + } + + ret = key_records_dump(&buf, &buf_size, &r, true); + key_records_clear(&r); + if (ret != KNOT_EOK) { + free(buf); + return ret; + } + print_header("Offline records for", from, buf); + empty = false; + + if (next == 0) { + break; + } + from = next; + } + free(buf); + + /* If from is lower than the first record's timestamp, try to start + from the first one's instead of empty output. */ + if (empty && from > 0) { + knot_time_t last = 0; + int ret = key_records_last_timestamp(ctx, &last); + if (ret == KNOT_EOK && knot_time_cmp(last, from) > 0) { + return keymgr_print_offline_records(ctx, 0, arg_to); + } + } + return KNOT_EOK; +} + +int keymgr_delete_offline_records(kdnssec_ctx_t *ctx, char *arg_from, char *arg_to) +{ + knot_time_t from, to; + int ret = parse_timestamp(arg_from, &from); + if (ret != KNOT_EOK) { + return ret; + } + ret = parse_timestamp(arg_to, &to); + if (ret != KNOT_EOK) { + return ret; + } + return kasp_db_delete_offline_records(ctx->kasp_db, ctx->zone->dname, from, to); +} + +int keymgr_del_all_old(kdnssec_ctx_t *ctx) +{ + for (size_t i = 0; i < ctx->zone->num_keys; ) { + knot_kasp_key_t *key = &ctx->zone->keys[i]; + if (knot_time_cmp(key->timing.remove, ctx->now) < 0) { + int ret = kdnssec_delete_key(ctx, key); + if (ret != KNOT_EOK) { + return ret; + } + } else { + i++; + } + } + return kdnssec_ctx_commit(ctx); +} + +static void print_generated_message(void) +{ + char buf[64] = { 0 }; + knot_time_print(TIME_PRINT_ISO8601, knot_time(), buf, sizeof(buf)); + printf("generated at %s by Knot DNS %s\n", buf, VERSION); +} + +static int ksr_once(kdnssec_ctx_t *ctx, char **buf, size_t *buf_size, knot_time_t *next_ksr) +{ + knot_rrset_t *dnskey = NULL; + zone_keyset_t keyset = { 0 }; + int ret = load_dnskey_rrset(ctx, &dnskey, &keyset); + if (ret != KNOT_EOK) { + goto done; + } + ret = dump_rrset_to_buf(dnskey, buf, buf_size); + if (ret >= 0) { + print_header("KeySigningRequest "KSR_SKR_VER, ctx->now, *buf); + ret = KNOT_EOK; + } + +done: + if (ret == KNOT_EOK && next_ksr != NULL) { + *next_ksr = knot_get_next_zone_key_event(&keyset); + } + knot_rrset_free(dnskey, NULL); + free_zone_keys(&keyset); + return ret; +} + +#define OFFLINE_KSK_CONF_CHECK \ + if (!ctx->policy->offline_ksk || !ctx->policy->manual) { \ + ERR2("offline-ksk and manual must be enabled in configuration"); \ + return KNOT_ESEMCHECK; \ + } + +int keymgr_print_ksr(kdnssec_ctx_t *ctx, char *arg_from, char *arg_to) +{ + OFFLINE_KSK_CONF_CHECK + + knot_time_t from, to; + int ret = parse_timestamp(arg_to, &to); + if (ret != KNOT_EOK) { + return ret; + } + if (arg_from == NULL) { + ret = key_records_last_timestamp(ctx, &from); + } else { + ret = parse_timestamp(arg_from, &from); + } + if (ret != KNOT_EOK) { + return ret; + } + + char *buf = NULL; + size_t buf_size = 4096; + while (ret == KNOT_EOK && knot_time_cmp(from, to) < 0) { + ctx->now = from; + ret = ksr_once(ctx, &buf, &buf_size, &from); + } + if (ret != KNOT_EOK) { + free(buf); + return ret; + } + ctx->now = to; + // force end of period as a KSR timestamp + ret = ksr_once(ctx, &buf, &buf_size, NULL); + + printf(";; KeySigningRequest %s ", KSR_SKR_VER); + print_generated_message(); + + free(buf); + return ret; +} + +typedef struct { + int ret; + key_records_t r; + knot_time_t timestamp; + kdnssec_ctx_t *kctx; +} ksr_sign_ctx_t; + +static int ksr_sign_dnskey(kdnssec_ctx_t *ctx, knot_rrset_t *zsk, knot_time_t now, + knot_time_t *next_sign) +{ + zone_keyset_t keyset = { 0 }; + char *buf = NULL; + size_t buf_size = 4096; + knot_time_t rrsigs_expire = 0; + + ctx->now = now; + ctx->policy->dnskey_ttl = zsk->ttl; + + int ret = load_zone_keys(ctx, &keyset, false); + if (ret != KNOT_EOK) { + return ret; + } + + key_records_t r; + key_records_init(ctx, &r); + + ret = knot_zone_sign_add_dnskeys(&keyset, ctx, &r, NULL, NULL); + if (ret != KNOT_EOK) { + goto done; + } + + ret = knot_rdataset_merge(&r.dnskey.rrs, &zsk->rrs, NULL); + if (ret != KNOT_EOK) { + goto done; + } + + // no check if the KSK used for signing (in keyset) is contained in DNSKEY record being signed (in KSR) ! + for (int i = 0; i < keyset.count; i++) { + ret = key_records_sign(&keyset.keys[i], &r, ctx, &rrsigs_expire); + if (ret != KNOT_EOK) { + goto done; + } + } + ret = key_records_dump(&buf, &buf_size, &r, true); + if (ret == KNOT_EOK) { + print_header("SignedKeyResponse "KSR_SKR_VER, ctx->now, buf); + *next_sign = knot_time_min( + knot_get_next_zone_key_event(&keyset), + knot_time_add(rrsigs_expire, -(knot_timediff_t)ctx->policy->rrsig_refresh_before) + ); + } + +done: + free(buf); + key_records_clear(&r); + free_zone_keys(&keyset); + return ret; +} + +static int process_skr_between_ksrs(ksr_sign_ctx_t *ctx, knot_time_t from, knot_time_t to) +{ + for (knot_time_t t = from; t < to /* if (t == infinity) stop */; ) { + int ret = ksr_sign_dnskey(ctx->kctx, &ctx->r.dnskey, t, &t); + if (ret != KNOT_EOK) { + return ret; + } + } + return KNOT_EOK; +} + +static void ksr_sign_header(zs_scanner_t *sc) +{ + ksr_sign_ctx_t *ctx = sc->process.data; + + // parse header + float header_ver; + knot_time_t next_timestamp = 0; + if (sc->error.code != 0 || ctx->ret != KNOT_EOK || + sscanf((const char *)sc->buffer, "; KeySigningRequest %f %"PRIu64, + &header_ver, &next_timestamp) < 1) { + return; + } + (void)header_ver; + + // sign previous KSR and inbetween KSK changes + if (ctx->timestamp > 0) { + knot_time_t inbetween_from; + ctx->ret = ksr_sign_dnskey(ctx->kctx, &ctx->r.dnskey, ctx->timestamp, + &inbetween_from); + if (next_timestamp > 0 && ctx->ret == KNOT_EOK) { + ctx->ret = process_skr_between_ksrs(ctx, inbetween_from, + next_timestamp); + } + key_records_clear_rdatasets(&ctx->r); + } + + // start new KSR + ctx->timestamp = next_timestamp; +} + +static void ksr_sign_once(zs_scanner_t *sc) +{ + ksr_sign_ctx_t *ctx = sc->process.data; + if (sc->error.code == 0 && ctx->ret == KNOT_EOK) { + ctx->ret = knot_rrset_add_rdata(&ctx->r.dnskey, sc->r_data, sc->r_data_length, NULL); + ctx->r.dnskey.ttl = sc->r_ttl; + } +} + +static void skr_import_header(zs_scanner_t *sc) +{ + ksr_sign_ctx_t *ctx = sc->process.data; + + // parse header + float header_ver; + knot_time_t next_timestamp; + if (sc->error.code != 0 || ctx->ret != KNOT_EOK || + sscanf((const char *)sc->buffer, "; SignedKeyResponse %f %"PRIu64, + &header_ver, &next_timestamp) < 1) { + return; + } + (void)header_ver; + + // delete possibly existing conflicting offline records + ctx->ret = kasp_db_delete_offline_records( + ctx->kctx->kasp_db, ctx->kctx->zone->dname, next_timestamp, 0 + ); + + // store previous SKR + if (ctx->timestamp > 0 && ctx->ret == KNOT_EOK) { + ctx->ret = key_records_verify(&ctx->r, ctx->kctx, ctx->timestamp); + if (ctx->ret != KNOT_EOK) { + return; + } + + ctx->ret = kasp_db_store_offline_records(ctx->kctx->kasp_db, + ctx->timestamp, &ctx->r); + key_records_clear_rdatasets(&ctx->r); + } + + // start new SKR + ctx->timestamp = next_timestamp; +} + +static void skr_validate_header(zs_scanner_t *sc) +{ + ksr_sign_ctx_t *ctx = sc->process.data; + + float header_ver; + knot_time_t next_timestamp; + if (sc->error.code != 0 || ctx->ret != KNOT_EOK || + sscanf((const char *)sc->buffer, "; SignedKeyResponse %f %"PRIu64, + &header_ver, &next_timestamp) < 1) { + return; + } + (void)header_ver; + + if (ctx->timestamp > 0 && ctx->ret == KNOT_EOK) { + int ret = key_records_verify(&ctx->r, ctx->kctx, ctx->timestamp); + if (ret != KNOT_EOK) { // ctx->ret untouched + ERR2("invalid SignedKeyResponse for %"KNOT_TIME_PRINTF" (%s)", + ctx->timestamp, knot_strerror(ret)); + } + key_records_clear_rdatasets(&ctx->r); + } + + ctx->timestamp = next_timestamp; +} + +static void skr_import_once(zs_scanner_t *sc) +{ + ksr_sign_ctx_t *ctx = sc->process.data; + if (sc->error.code == 0 && ctx->ret == KNOT_EOK) { + ctx->ret = key_records_add_rdata(&ctx->r, sc->r_type, sc->r_data, + sc->r_data_length, sc->r_ttl); + } +} + +static int read_ksr_skr(kdnssec_ctx_t *ctx, const char *infile, + void (*cb_header)(zs_scanner_t *), void (*cb_record)(zs_scanner_t *)) +{ + zs_scanner_t sc = { 0 }; + int ret = zs_init(&sc, "", KNOT_CLASS_IN, 0); + if (ret < 0) { + return KNOT_ERROR; + } + + ret = zs_set_input_file(&sc, infile); + if (ret < 0) { + zs_deinit(&sc); + return (sc.error.code == ZS_FILE_ACCESS) ? KNOT_EACCES : KNOT_EFILE; + } + + ksr_sign_ctx_t pctx = { 0 }; + key_records_init(ctx, &pctx.r); + pctx.kctx = ctx; + ret = zs_set_processing(&sc, cb_record, NULL, &pctx); + if (ret < 0) { + zs_deinit(&sc); + return KNOT_EBUSY; + } + sc.process.comment = cb_header; + + ret = zs_parse_all(&sc); + + if (sc.error.code != 0) { + ret = KNOT_EMALF; + } else if (pctx.ret != KNOT_EOK) { + ret = pctx.ret; + } else if (ret < 0 || pctx.r.dnskey.rrs.count > 0 || pctx.r.cdnskey.rrs.count > 0 || + pctx.r.cds.rrs.count > 0 || pctx.r.rrsig.rrs.count > 0) { + ret = KNOT_EMALF; + } + key_records_clear(&pctx.r); + zs_deinit(&sc); + return ret; +} + +int keymgr_sign_ksr(kdnssec_ctx_t *ctx, const char *ksr_file) +{ + OFFLINE_KSK_CONF_CHECK + + int ret = read_ksr_skr(ctx, ksr_file, ksr_sign_header, ksr_sign_once); + printf(";; SignedKeyResponse %s ", KSR_SKR_VER); + print_generated_message(); + return ret; +} + +int keymgr_import_skr(kdnssec_ctx_t *ctx, const char *skr_file) +{ + OFFLINE_KSK_CONF_CHECK + + return read_ksr_skr(ctx, skr_file, skr_import_header, skr_import_once); +} + +int keymgr_validate_skr(kdnssec_ctx_t *ctx, const char *skr_file) +{ + return read_ksr_skr(ctx, skr_file, skr_validate_header, skr_import_once); +} |