diff --git a/Knot.files b/Knot.files index 767e039eda55207c2dce6975dc8479ed6cf577d1..3f181255cf55b6052ca83aeab9c9d100883b895b 100644 --- a/Knot.files +++ b/Knot.files @@ -398,6 +398,8 @@ src/utils/keymgr/bind_privkey.h src/utils/keymgr/functions.c src/utils/keymgr/functions.h src/utils/keymgr/main.c +src/utils/keymgr/offline_ksk.c +src/utils/keymgr/offline_ksk.h src/utils/khost/khost_main.c src/utils/khost/khost_params.c src/utils/khost/khost_params.h diff --git a/doc/man/keymgr.8in b/doc/man/keymgr.8in index 416318358c6f83c7cb79cb64c04396108a628179..c49bf145dd3e0303a79af943a8c65ff57831924a 100644 --- a/doc/man/keymgr.8in +++ b/doc/man/keymgr.8in @@ -119,6 +119,18 @@ Remove the specified key from zone. If the key was not shared, it is also delete \fBshare\fP \fIkey_ID\fP Import a key (specified by full key ID) from another zone as shared. After this, the key is owned by both zones equally. +.UNINDENT +.SS Commands related to Offline KSK feature +.INDENT 0.0 +.TP +\fBpregenerate\fP \fIperiod_secs\fP +Pre\-generate ZSKs for use with offline KSK, for the specified period starting from now. +.TP +\fBpresign\fP \fIperiod_secs\fP +Sign pre\-generated ZSKs, for the specified period starting from now. +.TP +\fBshow\-rrsig\fP \fItimestamp\fP +Print a pre\-generated DNSKEY RRSIG for specified timestamp. .TP \fBdel\-all\-old\fP Delete old keys that are in state \(aqremoved\(aq. diff --git a/doc/man_keymgr.rst b/doc/man_keymgr.rst index 5d14c042d2dc8f2e1d62f8067c3dd0227bc01c8e..9f0552a36c78a5c0431cee9ace7dae5bb05921dc 100644 --- a/doc/man_keymgr.rst +++ b/doc/man_keymgr.rst @@ -97,6 +97,18 @@ Commands Import a key (specified by full key ID) from another zone as shared. After this, the key is owned by both zones equally. +Commands related to Offline KSK feature +....................................... + +**pregenerate** *period_secs* + Pre-generate ZSKs for use with offline KSK, for the specified period starting from now. + +**presign** *period_secs* + Sign pre-generated ZSKs, for the specified period starting from now. + +**show-rrsig** *timestamp* + Print a pre-generated DNSKEY RRSIG for specified timestamp. + **del-all-old** Delete old keys that are in state 'removed'. diff --git a/src/knot/dnssec/context.h b/src/knot/dnssec/context.h index 2d7da371277eeb0ab9a961e9b4ccabe25f9524d1..795c1235fbbe4c4f2fbe59ec78dfe635d6b99d7c 100644 --- a/src/knot/dnssec/context.h +++ b/src/knot/dnssec/context.h @@ -38,6 +38,8 @@ typedef struct { char *kasp_zone_path; bool rrsig_drop_existing; + bool keep_deleted_keys; + bool rollover_only_zsk; } kdnssec_ctx_t; /*! diff --git a/src/knot/dnssec/kasp/kasp_db.c b/src/knot/dnssec/kasp/kasp_db.c index 2d4235903a91e8540b2292c29a9eb6591903dc94..a342ed96c0e7bc05b0e96bb06294ebceba5037c5 100644 --- a/src/knot/dnssec/kasp/kasp_db.c +++ b/src/knot/dnssec/kasp/kasp_db.c @@ -22,6 +22,7 @@ #include "contrib/files.h" #include "contrib/wire_ctx.h" +#include "knot/journal/serialization.h" struct kasp_db { knot_db_t *keys_db; @@ -31,6 +32,7 @@ struct kasp_db { }; typedef enum { + KASPDBKEY_OFFLINE_RRSIG = 0x0, // this MUST be always first, because we use LEQ operator KASPDBKEY_PARAMS = 0x1, KASPDBKEY_POLICYLAST = 0x2, KASPDBKEY_NSEC3SALT = 0x3, @@ -749,3 +751,54 @@ int kasp_db_list_zones(kasp_db_t *db, list_t *dst) } return (EMPTY_LIST(*dst) ? KNOT_ENOENT : KNOT_EOK); } + +static void for_time2string(char str[21], knot_time_t t) +{ + snprintf(str, 21, "%020lu", t); +} + +int kasp_db_store_offline_rrsig(kasp_db_t *db, knot_time_t for_time, const knot_rrset_t *rrsig) +{ + if (db == NULL || rrsig == NULL || rrsig->type != KNOT_RRTYPE_RRSIG) { + return KNOT_EINVAL; + } + + char for_time_str[21]; + for_time2string(for_time_str, for_time); + knot_db_val_t key = make_key(KASPDBKEY_OFFLINE_RRSIG, rrsig->owner, for_time_str), val; + val.len = rrset_serialized_size(rrsig); + val.data = malloc(val.len); + if (val.data == NULL) { + free_key(&key); + return KNOT_ENOMEM; + } + with_txn(KEYS_RW, NULL); + wire_ctx_t wire = wire_ctx_init(val.data, val.len); + ret = serialize_rrset(&wire, rrsig); + if (ret == KNOT_EOK) { + ret = db_api->insert(txn, &key, &val, 0); + } + free_key(&key); + with_txn_end(NULL); + return ret; +} + +int kasp_db_load_offline_rrsig(kasp_db_t *db, const knot_dname_t *for_dname, knot_time_t for_time, knot_rrset_t *rrsig) +{ + if (db == NULL || rrsig == NULL) { + return KNOT_EINVAL; + } + + char for_time_str[21]; + for_time2string(for_time_str, for_time); + with_txn(KEYS_RO, NULL); + knot_db_val_t key = make_key(KASPDBKEY_OFFLINE_RRSIG, for_dname, for_time_str), val; + ret = db_api->find(txn, &key, &val, KNOT_DB_LEQ); + if (ret == KNOT_EOK) { + wire_ctx_t wire = wire_ctx_init(val.data, val.len); + ret = deserialize_rrset(&wire, rrsig); + } + free_key(&key); + with_txn_end(NULL); + return ret; +} diff --git a/src/knot/dnssec/kasp/kasp_db.h b/src/knot/dnssec/kasp/kasp_db.h index a80c4b458f5367604d6e3aa7d8f6d17febe3c318..c22b5bb3387d13e4cb8303e3e2c3378340f17ead 100644 --- a/src/knot/dnssec/kasp/kasp_db.h +++ b/src/knot/dnssec/kasp/kasp_db.h @@ -233,3 +233,26 @@ int kasp_db_set_policy_last(kasp_db_t *db, const char *policy_string, const char * \return KNOT_E* */ int kasp_db_list_zones(kasp_db_t *db, list_t *dst); + +/*! + * \brief Store pre-generated RRSIG for offline KSK usage. + * + * \param db KASP db. + * \param for_time Timestamp in future in which the RRSIG shall be used. + * \param rrsig The rrsig to be stored. + * + * \return KNOT_E* + */ +int kasp_db_store_offline_rrsig(kasp_db_t *db, knot_time_t for_time, const knot_rrset_t *rrsig); + +/*! + * \brief Load pregenerated RRSIG. + * + * \param db KASP db. + * \param for_dname Name of the related zone. + * \param for_time Now. Closest RRSIG (timestamp equals or is closest lower). + * \param rrsig Output: the RRSIG. + * + * \return KNOT_E* + */ +int kasp_db_load_offline_rrsig(kasp_db_t *db, const knot_dname_t *for_dname, knot_time_t for_time, knot_rrset_t *rrsig); diff --git a/src/knot/dnssec/key-events.c b/src/knot/dnssec/key-events.c index ee2c247ab94ded2e4a71e50017b33e6a8c1eee1c..2695993c9f9fe9b9d771e19469ba0d864aff9ae4 100644 --- a/src/knot/dnssec/key-events.c +++ b/src/knot/dnssec/key-events.c @@ -368,6 +368,10 @@ static roll_action_t next_action(kdnssec_ctx_t *ctx, zone_sign_roll_flags_t flag continue; } if (key->is_ksk) { + if (ctx->rollover_only_zsk) { + continue; + } + switch (get_key_state(key, ctx->now)) { case DNSSEC_KEY_STATE_PRE_ACTIVE: keytime = alg_publish_time(key->timing.pre_active, ctx); @@ -435,11 +439,14 @@ static roll_action_t next_action(kdnssec_ctx_t *ctx, zone_sign_roll_flags_t flag keytime = alg_remove_time(key->timing.post_active, ctx); restype = REMOVE; break; - case DNSSEC_KEY_STATE_RETIRED: case DNSSEC_KEY_STATE_REMOVED: // ad REMOVED state: normally this wouldn't happen // (key in removed state is instantly deleted) // but if imported keys, they can be in this state + if (ctx->keep_deleted_keys) { + break; + } // else FALLBACK + case DNSSEC_KEY_STATE_RETIRED: keytime = knot_time_min(key->timing.retire, key->timing.remove); keytime = ksk_remove_time(keytime, ctx);; restype = REMOVE; @@ -557,7 +564,11 @@ static int exec_remove_old_key(kdnssec_ctx_t *ctx, knot_kasp_key_t *key) get_key_state(key, ctx->now) == DNSSEC_KEY_STATE_REMOVED); key->timing.remove = ctx->now; - return kdnssec_delete_key(ctx, key); + if (ctx->keep_deleted_keys) { + return KNOT_EOK; + } else { + return kdnssec_delete_key(ctx, key); + } } int knot_dnssec_key_rollover(kdnssec_ctx_t *ctx, zone_sign_roll_flags_t flags, @@ -616,7 +627,8 @@ int knot_dnssec_key_rollover(kdnssec_ctx_t *ctx, zone_sign_roll_flags_t flags, } } // algorithm rollover - if (algorithm_present(ctx, ctx->policy->algorithm) == 0 && + if (!ctx->rollover_only_zsk && + algorithm_present(ctx, ctx->policy->algorithm) == 0 && !running_rollover(ctx) && allowed_general_roll && ret == KNOT_EOK) { ret = generate_ksk(ctx, 0, true); if (!ctx->policy->singe_type_signing && ret == KNOT_EOK) { @@ -628,7 +640,8 @@ int knot_dnssec_key_rollover(kdnssec_ctx_t *ctx, zone_sign_roll_flags_t flags, } } // scheme rollover - if (!signing_scheme_present(ctx) && allowed_general_roll && + if (!ctx->rollover_only_zsk && + !signing_scheme_present(ctx) && allowed_general_roll && !running_rollover(ctx) && ret == KNOT_EOK) { ret = generate_ksk(ctx, 0, false); if (!ctx->policy->singe_type_signing && ret == KNOT_EOK) { diff --git a/src/knot/dnssec/zone-keys.c b/src/knot/dnssec/zone-keys.c index 63211808c7d1c8dd89136a44d75f13e93e66b094..4c06804da22cf19dbab3acf3f082d4f5f47e67ff 100644 --- a/src/knot/dnssec/zone-keys.c +++ b/src/knot/dnssec/zone-keys.c @@ -319,7 +319,13 @@ static int load_private_keys(dnssec_keystore_t *keystore, zone_keyset_t *keyset) zone_key_t *key = &keyset->keys[i]; int r = dnssec_key_import_keystore(key->key, keystore, key->id); - if (r != DNSSEC_EOK && r != DNSSEC_KEY_ALREADY_PRESENT) { + switch (r) { + case DNSSEC_EOK: + case DNSSEC_KEY_ALREADY_PRESENT: + break; + case DNSSEC_ENOENT: // we hope that this is just offline KSK + break; + default: return r; } } diff --git a/src/knot/dnssec/zone-sign.c b/src/knot/dnssec/zone-sign.c index 4e17274145234085784bc5bda186bb17af2dedd3..3e2095a2929e8a15b0bd422771a5f13e5cf04532 100644 --- a/src/knot/dnssec/zone-sign.c +++ b/src/knot/dnssec/zone-sign.c @@ -21,6 +21,7 @@ #include "libdnssec/key.h" #include "libdnssec/keytag.h" #include "libdnssec/sign.h" +#include "knot/common/log.h" #include "knot/dnssec/key-events.h" #include "knot/dnssec/rrset-sign.h" #include "knot/dnssec/zone-sign.h" @@ -102,7 +103,8 @@ static bool valid_signature_exists(const knot_rrset_t *covered, const knot_rrset_t *rrsigs, const dnssec_key_t *key, dnssec_sign_ctx_t *ctx, - const kdnssec_ctx_t *dnssec_ctx) + const kdnssec_ctx_t *dnssec_ctx, + uint16_t *at) { assert(key); @@ -124,6 +126,9 @@ static bool valid_signature_exists(const knot_rrset_t *covered, if (knot_check_signature(covered, rrsigs, i, key, ctx, dnssec_ctx) == KNOT_EOK) { + if (at != NULL) { + *at = i; + } return true; } } @@ -156,7 +161,7 @@ static bool all_signatures_exist(const knot_rrset_t *covered, } if (!valid_signature_exists(covered, rrsigs, key->key, - key->ctx, dnssec_ctx)) { + key->ctx, dnssec_ctx, NULL)) { return false; } } @@ -272,6 +277,28 @@ static int remove_expired_rrsigs(const knot_rrset_t *covered, return result; } +static bool can_have_offline_rrsig(const knot_rrset_t *rr, const knot_dname_t *zone_apex) +{ + return (rr->type == KNOT_RRTYPE_DNSKEY && knot_dname_cmp(rr->owner, zone_apex) == 0); +} + +static int load_offline_rrsig(const knot_rrset_t *covered, + knot_rrset_t *rrsig, + const kdnssec_ctx_t *ctx) +{ + knot_rrset_init_empty(rrsig); + + if (!can_have_offline_rrsig(covered, ctx->zone->dname)) { + return KNOT_EOK; + } + + int ret = kasp_db_load_offline_rrsig(*ctx->kasp_db, covered->owner, ctx->now, rrsig); + if (ret == KNOT_ENOENT) { + ret = KNOT_EOK; + } + return ret; +} + /*! * \brief Add missing RRSIGs into the changeset for adding. * @@ -295,9 +322,13 @@ static int add_missing_rrsigs(const knot_rrset_t *covered, assert(zone_keys); assert(changeset); - int result = KNOT_EOK; - knot_rrset_t to_add; + knot_rrset_t to_add, offline_rrsigs; knot_rrset_init_empty(&to_add); + int result = load_offline_rrsig(covered, &offline_rrsigs, dnssec_ctx); + if (result != KNOT_EOK) { + log_zone_warning(dnssec_ctx->zone->dname, "DNSSEC, failed to load offline DNSKEY RRSIG (%s)", + knot_strerror(result)); + } for (int i = 0; i < zone_keys->count; i++) { const zone_key_t *key = &zone_keys->keys[i]; @@ -305,7 +336,7 @@ static int add_missing_rrsigs(const knot_rrset_t *covered, continue; } - if (valid_signature_exists(covered, rrsigs, key->key, key->ctx, dnssec_ctx)) { + if (valid_signature_exists(covered, rrsigs, key->key, key->ctx, dnssec_ctx, NULL)) { continue; } @@ -313,6 +344,17 @@ static int add_missing_rrsigs(const knot_rrset_t *covered, to_add = create_empty_rrsigs_for(covered); } + uint16_t at_offline; + if (valid_signature_exists(covered, &offline_rrsigs, key->key, key->ctx, dnssec_ctx, &at_offline)) { + log_zone_info(dnssec_ctx->zone->dname, "DNSSEC, using offline DNSKEY RRSIG"); + knot_rdata_t *offline_rd = knot_rdataset_at(&offline_rrsigs.rrs, at_offline); + result = knot_rrset_add_rdata(&to_add, offline_rd->data, offline_rd->len, NULL); + if (result != KNOT_EOK) { + break; + } + continue; + } + result = knot_sign_rrset(&to_add, covered, key->key, key->ctx, dnssec_ctx, NULL, expires_at); if (result != KNOT_EOK) { @@ -325,6 +367,7 @@ static int add_missing_rrsigs(const knot_rrset_t *covered, } knot_rdataset_clear(&to_add.rrs, NULL); + knot_rrset_clear(&offline_rrsigs, NULL); return result; } @@ -588,7 +631,7 @@ typedef struct { /*- private API - DNSKEY handling --------------------------------------------*/ -static int rrset_add_zone_key(knot_rrset_t *rrset, zone_key_t *zone_key) +int rrset_add_zone_key(knot_rrset_t *rrset, zone_key_t *zone_key) { assert(rrset); assert(zone_key); diff --git a/src/knot/dnssec/zone-sign.h b/src/knot/dnssec/zone-sign.h index 8796c4d5373f79cc67d23219922623928fba28df..7f833daef26a074d67c878d5e96935b5ee18c811 100644 --- a/src/knot/dnssec/zone-sign.h +++ b/src/knot/dnssec/zone-sign.h @@ -22,6 +22,8 @@ #include "knot/dnssec/context.h" #include "knot/dnssec/zone-keys.h" +int rrset_add_zone_key(knot_rrset_t *rrset, zone_key_t *zone_key); + /*! * \brief Adds/removes DNSKEY (and CDNSKEY, CDS) records to zone according to zone keyset. * diff --git a/src/knot/journal/serialization.c b/src/knot/journal/serialization.c index ae91b2f6ccde7d45838c82ece05f20042eb76a11..d764fe606cc49efa5ae15c3686d990d3fc528fe3 100644 --- a/src/knot/journal/serialization.c +++ b/src/knot/journal/serialization.c @@ -190,7 +190,7 @@ void serialize_deinit(serialize_ctx_t *ctx) free(ctx); } -static int deserialize_rrset(wire_ctx_t *wire, knot_rrset_t *rrset, long *phase) +static int _deserialize_rrset(wire_ctx_t *wire, knot_rrset_t *rrset, long *phase) { assert(wire != NULL && rrset != NULL && phase != NULL); assert(*phase >= SERIALIZE_RRSET_INIT && *phase < SERIALIZE_RRSET_DONE); @@ -245,7 +245,7 @@ int deserialize_rrset_chunks(wire_ctx_t *wire, knot_rrset_t *rrset, { long phase = SERIALIZE_RRSET_INIT; while (1) { - int ret = deserialize_rrset(wire, rrset, &phase); + int ret = _deserialize_rrset(wire, rrset, &phase); if (ret != KNOT_EOK || phase == SERIALIZE_RRSET_DONE) { return ret; } @@ -378,3 +378,93 @@ int changeset_deserialize(changeset_t *ch, uint8_t *src_chunks[], return wire.error; } + +int serialize_rrset(wire_ctx_t *wire, const knot_rrset_t *rrset) +{ + assert(wire != NULL && rrset != NULL); + + // write owner, type, class, rrcnt + int size = knot_dname_to_wire(wire->position, rrset->owner, + wire_ctx_available(wire)); + if (size < 0 || wire_ctx_available(wire) < size + 3 * sizeof(uint16_t)) { + assert(0); + } + wire_ctx_skip(wire, size); + wire_ctx_write_u16(wire, rrset->type); + wire_ctx_write_u16(wire, rrset->rclass); + wire_ctx_write_u16(wire, rrset->rrs.count); + + for (size_t phase = 0; phase < rrset->rrs.count; phase++) { + const knot_rdata_t *rr = knot_rdataset_at(&rrset->rrs, phase); + assert(rr); + uint16_t rdlen = rr->len; + if (wire_ctx_available(wire) < sizeof(uint32_t) + sizeof(uint16_t) + rdlen) { + assert(0); + } + wire_ctx_write_u32(wire, rrset->ttl); + wire_ctx_write_u16(wire, rdlen); + wire_ctx_write(wire, rr->data, rdlen); + } + + return KNOT_EOK; +} + +int deserialize_rrset(wire_ctx_t *wire, knot_rrset_t *rrset) +{ + assert(wire != NULL && rrset != NULL); + + // Read owner, rtype, rclass and RR count. + int size = knot_dname_size(wire->position); + if (size < 0) { + assert(0); + } + knot_dname_t *owner = knot_dname_copy(wire->position, NULL); + if (owner == NULL || wire_ctx_available(wire) < size + 3 * sizeof(uint16_t)) { + return KNOT_EMALF; + } + wire_ctx_skip(wire, size); + uint16_t type = wire_ctx_read_u16(wire); + uint16_t rclass = wire_ctx_read_u16(wire); + uint16_t rrcount = wire_ctx_read_u16(wire); + if (wire->error != KNOT_EOK) { + return wire->error; + } + knot_rrset_init(rrset, owner, type, rclass, 0); + + for (size_t phase = 0; phase < rrcount && wire_ctx_available(wire) > 0; phase++) { + uint32_t ttl = wire_ctx_read_u32(wire); + uint32_t rdata_size = wire_ctx_read_u16(wire); + if (phase == 0) { + rrset->ttl = ttl; + } + if (wire->error != KNOT_EOK || + wire_ctx_available(wire) < rdata_size || + knot_rrset_add_rdata(rrset, wire->position, rdata_size, + NULL) != KNOT_EOK) { + knot_rrset_clear(rrset, NULL); + return KNOT_EMALF; + } + wire_ctx_skip(wire, rdata_size); + } + + return KNOT_EOK; +} + +size_t rrset_serialized_size(const knot_rrset_t *rrset) +{ + if (rrset == NULL || rrset->rrs.count == 0) { + return 0; + } + + // Owner size + type + class + RR count. + size_t size = knot_dname_size(rrset->owner) + 3 * sizeof(uint16_t); + + for (uint16_t i = 0; i < rrset->rrs.count; i++) { + const knot_rdata_t *rr = knot_rdataset_at(&rrset->rrs, i); + assert(rr); + // TTL + RR size + RR. + size += sizeof(uint32_t) + sizeof(uint16_t) + rr->len; + } + + return size; +} diff --git a/src/knot/journal/serialization.h b/src/knot/journal/serialization.h index 04afe394b683d84ef532078d10310531f48c30ef..bc1202711d5ee12f872d32a236ea11ee5bdba05f 100644 --- a/src/knot/journal/serialization.h +++ b/src/knot/journal/serialization.h @@ -96,3 +96,32 @@ int changeset_deserialize(changeset_t *ch, uint8_t *src_chunks[], int deserialize_rrset_chunks(wire_ctx_t *wire, knot_rrset_t *rrset, uint8_t *src_chunks[], const size_t *chunk_sizes, size_t chunks_count, size_t *cur_chunk); + +/*! + * \brief Simply serialize RRset w/o any chunking. + * + * \param wire + * \param rrset + * + * \return KNOT_E* + */ +int serialize_rrset(wire_ctx_t *wire, const knot_rrset_t *rrset); + +/*! + * \brief Simply deserialize RRset w/o any chunking. + * + * \param wire + * \param rrset + * + * \return KNOT_E* + */ +int deserialize_rrset(wire_ctx_t *wire, knot_rrset_t *rrset); + +/*! + * \brief Space needed to serialize RRset. + * + * \param rrset RRset. + * + * \return RRset binary size. + */ +size_t rrset_serialized_size(const knot_rrset_t *rrset); diff --git a/src/utils/Makefile.inc b/src/utils/Makefile.inc index abb86990061594f232d4c88648921ff556300d0b..1e3f91daa9a3b61febbbdba698a7a82ab169d867 100644 --- a/src/utils/Makefile.inc +++ b/src/utils/Makefile.inc @@ -123,6 +123,8 @@ keymgr_SOURCES = \ utils/keymgr/bind_privkey.h \ utils/keymgr/functions.c \ utils/keymgr/functions.h \ + utils/keymgr/offline_ksk.c \ + utils/keymgr/offline_ksk.h \ utils/keymgr/main.c kjournalprint_SOURCES = \ diff --git a/src/utils/keymgr/functions.c b/src/utils/keymgr/functions.c index b8dbecf7dd335e43c489945e4584c0dcc0302539..fd9ecf5f06ab91be0dc77fb82c5bb73a224b38dc 100644 --- a/src/utils/keymgr/functions.c +++ b/src/utils/keymgr/functions.c @@ -828,15 +828,3 @@ int keymgr_generate_dnskey(const knot_dname_t *dname, const knot_kasp_key_t *key free(name); return KNOT_EOK; } - -int keymgr_del_all_old(kdnssec_ctx_t *ctx) -{ - for (size_t i = 0; i < ctx->zone->num_keys; i++) { - 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); - printf("- %s\n", knot_strerror(ret)); - } - } - return kdnssec_ctx_commit(ctx); -} diff --git a/src/utils/keymgr/functions.h b/src/utils/keymgr/functions.h index 2fbdc56f11274918244b6b02b9e2aaab1a9bddf9..dda367f9399d95708ec534e849dcc38acb81133b 100644 --- a/src/utils/keymgr/functions.h +++ b/src/utils/keymgr/functions.h @@ -39,5 +39,3 @@ int keymgr_list_keys(kdnssec_ctx_t *ctx, knot_time_print_t format); int keymgr_generate_ds(const knot_dname_t *dname, const knot_kasp_key_t *key); int keymgr_generate_dnskey(const knot_dname_t *dname, const knot_kasp_key_t *key); - -int keymgr_del_all_old(kdnssec_ctx_t *ctx); diff --git a/src/utils/keymgr/main.c b/src/utils/keymgr/main.c index 47f08e7f223e692de3fde300974fe450e512c616..cd629deaa60baac913f7d433c349bc3c6a3ae21c 100644 --- a/src/utils/keymgr/main.c +++ b/src/utils/keymgr/main.c @@ -27,6 +27,7 @@ #include "libknot/libknot.h" #include "utils/common/params.h" #include "utils/keymgr/functions.h" +#include "utils/keymgr/offline_ksk.h" #define PROGRAM_NAME "keymgr" @@ -69,6 +70,14 @@ static void print_help(void) " (syntax: delete <key_spec>)\n" " set Set existing key's timing attribute.\n" " (syntax: set <key_spec> <attribute_name>=<value>...)\n" + "\n" + "Commands related to Offline KSK feature:\n" + " pregenerate Pre-generate ZSKs for later rollovers with offline KSK.\n" + " (syntax: pregenerate <period_secs>)\n" + " presign Pre-generate RRSIG signatures for pregenerated ZSKs.\n" + " (syntax: presign <period_secs>)\n" + " show-rrsig Print a pre-generated DNSKEY RRSIG for specified timestamp.\n" + " (syntax: show-rrsig <timestamp>)\n" " del-all-old Delete old keys that are in state 'removed'.\n" "\n" "Key specification:\n" @@ -195,6 +204,15 @@ static int key_command(int argc, char *argv[], int optind) if (ret == KNOT_EOK) { ret = kdnssec_delete_key(&kctx, key2del); } + } else if (strcmp(argv[1], "pregenerate") == 0) { + CHECK_MISSING_ARG("Period not specified"); + ret = keymgr_pregenerate_zsks(&kctx, knot_time() + atol(argv[2])); + } else if (strcmp(argv[1], "presign") == 0) { + CHECK_MISSING_ARG("Timestamp not specified"); + ret = keymgr_presign_zsks(&kctx, knot_time() + atol(argv[2])); + } else if (strcmp(argv[1], "show-rrsig") == 0) { + CHECK_MISSING_ARG("Timestamp not specified"); + ret = keymgr_print_rrsig(&kctx, atol(argv[2])); } else if (strcmp(argv[1], "del-all-old") == 0) { ret = keymgr_del_all_old(&kctx); } else { diff --git a/src/utils/keymgr/offline_ksk.c b/src/utils/keymgr/offline_ksk.c new file mode 100644 index 0000000000000000000000000000000000000000..cf1e92a07d6059508a8bb74b26baa16d38f14485 --- /dev/null +++ b/src/utils/keymgr/offline_ksk.c @@ -0,0 +1,183 @@ +/* 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 <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/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" + +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) { + printf("rollover failed\n"); + 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; +} + +static void update_next_resign(knot_time_t *next, knot_time_t now, knot_time_t key_timer) +{ + if (knot_time_cmp(now, key_timer) < 0) { + *next = knot_time_min(*next, key_timer); + } +} + +static int presign_once(kdnssec_ctx_t *ctx, knot_time_t *next) +{ + // prepare the DNSKEY rrset to be signed + zone_keyset_t keyset = { 0 }; + int ret = load_zone_keys(ctx, &keyset, false); + if (ret != KNOT_EOK) { + printf("load keys failed\n"); + return ret; + } + knot_rrset_t *dnskey = knot_rrset_new(ctx->zone->dname, KNOT_RRTYPE_DNSKEY, KNOT_CLASS_IN, ctx->policy->dnskey_ttl, NULL); + knot_rrset_t *rrsig = knot_rrset_new(ctx->zone->dname, KNOT_RRTYPE_RRSIG, KNOT_CLASS_IN, ctx->policy->dnskey_ttl, NULL); + if (dnskey == NULL || rrsig == NULL) { + ret = KNOT_ENOMEM; + goto done; + } + 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) { + printf("add zone key failed\n"); + goto done; + } + } + } + + // sign the DNSKEY rrset + for (int i = 0; i < keyset.count; i++) { + zone_key_t *key = &keyset.keys[i]; + if (key->is_active && key->is_ksk) { + ret = knot_sign_rrset(rrsig, dnskey, key->key, key->ctx, ctx, NULL); + if (ret != KNOT_EOK) { + printf("sign rrset failed\n"); + goto done; + } + } + } + + // store it to KASP db + assert(!knot_rrset_empty(rrsig)); + ret = kasp_db_store_offline_rrsig(*ctx->kasp_db, ctx->now, rrsig); + if (ret != KNOT_EOK) { + printf("store rrsig failed\n"); + goto done; + } + + // next re-sign when rrsig expire or dnskey rrset changes + *next = ctx->now + ctx->policy->rrsig_lifetime - ctx->policy->rrsig_refresh_before; + for (int i = 0; i < ctx->zone->num_keys; i++) { + update_next_resign(next, ctx->now, ctx->zone->keys[i].timing.publish); + update_next_resign(next, ctx->now, ctx->zone->keys[i].timing.retire_active); + update_next_resign(next, ctx->now, ctx->zone->keys[i].timing.remove); + } + +done: + knot_rrset_free(dnskey, NULL); + knot_rrset_free(rrsig, NULL); + free_zone_keys(&keyset); + return ret; +} + +int keymgr_pregenerate_zsks(kdnssec_ctx_t *ctx, knot_time_t upto) +{ + knot_time_t next = ctx->now; + int ret = KNOT_EOK; + + ctx->keep_deleted_keys = true; + ctx->rollover_only_zsk = true; + ctx->policy->manual = false; + + while (ret == KNOT_EOK && knot_time_cmp(next, upto) <= 0) { + ctx->now = next; + printf("pregenerate %lu\n", ctx->now); + ret = pregenerate_once(ctx, &next); + } + + return ret; +} + +int keymgr_presign_zsks(kdnssec_ctx_t *ctx, knot_time_t upto) +{ + knot_time_t next = ctx->now; + int ret = KNOT_EOK; + + ctx->keep_deleted_keys = true; + ctx->policy->manual = true; + + while (ret == KNOT_EOK && knot_time_cmp(next, upto) <= 0) { + ctx->now = next; + printf("presign %lu\n", ctx->now); + ret = presign_once(ctx, &next); + } + + return ret; +} + +int keymgr_print_rrsig(kdnssec_ctx_t *ctx, knot_time_t when) +{ + knot_rrset_t rrsig = { 0 }; + knot_rrset_init_empty(&rrsig); + int ret = kasp_db_load_offline_rrsig(*ctx->kasp_db, ctx->zone->dname, when, &rrsig); + if (ret == KNOT_EOK) { + size_t out_size = 512; + char *out = malloc(out_size); + if (out == NULL) { + ret = KNOT_ENOMEM; + } else { + ret = knot_rrset_txt_dump(&rrsig, &out, &out_size, &KNOT_DUMP_STYLE_DEFAULT); + if (ret >= 0) { + printf("%s\n", out); + ret = KNOT_EOK; + } + free(out); + } + } + knot_rrset_clear(&rrsig, NULL); + return ret; +} + +int keymgr_del_all_old(kdnssec_ctx_t *ctx) +{ + for (size_t i = 0; i < ctx->zone->num_keys; i++) { + 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); + printf("- %s\n", knot_strerror(ret)); + } + } + return kdnssec_ctx_commit(ctx); +} diff --git a/src/utils/keymgr/offline_ksk.h b/src/utils/keymgr/offline_ksk.h new file mode 100644 index 0000000000000000000000000000000000000000..37c1fab85bbb5fefe79e4e9ef67b53bf8cd3668b --- /dev/null +++ b/src/utils/keymgr/offline_ksk.h @@ -0,0 +1,27 @@ +/* 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/>. +*/ + +#pragma once + +#include "knot/dnssec/context.h" + +int keymgr_pregenerate_zsks(kdnssec_ctx_t *ctx, knot_time_t upto); + +int keymgr_presign_zsks(kdnssec_ctx_t *ctx, knot_time_t upto); + +int keymgr_print_rrsig(kdnssec_ctx_t *ctx, knot_time_t when); + +int keymgr_del_all_old(kdnssec_ctx_t *ctx); diff --git a/tests-extra/tests/dnssec/offline_ksk/test.py b/tests-extra/tests/dnssec/offline_ksk/test.py new file mode 100644 index 0000000000000000000000000000000000000000..1ac056b58ac1ceb86d1198349159c4048ca16558 --- /dev/null +++ b/tests-extra/tests/dnssec/offline_ksk/test.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 + +""" +Test of offline KSK by trying pre-generated ZSK rollover. +""" + +import collections +import os +import shutil +import datetime +import subprocess +import time +from subprocess import check_call + +from dnstest.utils import * +from dnstest.keys import Keymgr +from dnstest.test import Test + +# check zone if keys are present and used for signing +def check_zone(server, zone, dnskeys, dnskey_rrsigs, soa_rrsigs, msg): + qdnskeys = server.dig("example.com", "DNSKEY", bufsize=4096) + found_dnskeys = qdnskeys.count("DNSKEY") + + qdnskeyrrsig = server.dig("example.com", "DNSKEY", dnssec=True, bufsize=4096) + found_rrsigs = qdnskeyrrsig.count("RRSIG") + + qsoa = server.dig("example.com", "SOA", dnssec=True, bufsize=4096) + found_soa_rrsigs = qsoa.count("RRSIG") + + check_log("DNSKEYs: %d (expected %d)" % (found_dnskeys, dnskeys)); + check_log("RRSIGs: %d (expected %d)" % (found_soa_rrsigs, soa_rrsigs)); + check_log("DNSKEY-RRSIGs: %d (expected %d)" % (found_rrsigs, dnskey_rrsigs)); + + if found_dnskeys != dnskeys: + set_err("BAD DNSKEY COUNT: " + msg) + detail_log("!DNSKEYs not published and activated as expected: " + msg) + + if found_soa_rrsigs != soa_rrsigs: + set_err("BAD RRSIG COUNT: " + msg) + detail_log("!RRSIGs not published and activated as expected: " + msg) + + if found_rrsigs != dnskey_rrsigs: + set_err("BAD DNSKEY RRSIG COUNT: " + msg) + detail_log("!RRSIGs not published and activated as expected: " + msg) + + detail_log(SEP) + + # Valgrind delay breaks the timing! + if not server.valgrind: + server.zone_backup(zone, flush=True) + server.zone_verify(zone) + +def wait_for_rrsig_count(t, server, rrtype, rrsig_count, timeout): + rtime = 0 + while True: + qdnskeyrrsig = server.dig("example.com", rrtype, dnssec=True, bufsize=4096) + found_rrsigs = qdnskeyrrsig.count("RRSIG") + if found_rrsigs == rrsig_count: + break + rtime = rtime + 1 + t.sleep(1) + if rtime > timeout: + break + +def wait_for_dnskey_count(t, server, dnskey_count, timeout): + for rtime in range(1, timeout): + qdnskeyrrsig = server.dig("example.com", "DNSKEY", dnssec=True, bufsize=4096) + found_dnskeys = qdnskeyrrsig.count("DNSKEY") + if found_dnskeys == dnskey_count: + break + t.sleep(1) + +t = Test() + +knot = t.server("knot") +ZONE = "example.com." +FUTURE = 100 + +zone = t.zone(ZONE) +t.link(zone, knot) + +knot.zonefile_sync = 24 * 60 * 60 + +knot.dnssec(zone).enable = True +knot.dnssec(zone).manual = True +knot.dnssec(zone).alg = "ECDSAP384SHA384" +knot.dnssec(zone).dnskey_ttl = 2 +knot.dnssec(zone).zsk_lifetime = 12 +knot.dnssec(zone).ksk_lifetime = 300 # this can be possibly left also infinity +knot.dnssec(zone).propagation_delay = 3 +knot.port = 1234 # dummy, will be overwritten +knot.gen_confile() + +key_ksk = knot.gen_key(zone, ksk=True, alg="ECDSAP384SHA384", key_len=384) +key_zsk = knot.gen_key(zone, ksk=False, alg="ECDSAP384SHA384", key_len=384) + +Keymgr.run_check(knot.confile, ZONE, "pregenerate", str(FUTURE)) +Keymgr.run_check(knot.confile, ZONE, "presign", str(FUTURE)) + +os.remove(knot.keydir + "/keys/" + key_ksk.keyid + ".pem") + +# parameters + +t.start() +knot.zone_wait(zone) +check_zone(knot, zone, 2, 1, 1, "init") + +wait_for_dnskey_count(t, knot, 3, knot.dnssec(zone).zsk_lifetime) +check_zone(knot, zone, 3, 1, 1, "ZSK rollover") + +t.sleep(2) + +wait_for_dnskey_count(t, knot, 2, 2 * (knot.dnssec(zone).propagation_delay + knot.dnssec(zone).dnskey_ttl)) +check_zone(knot, zone, 2, 1, 1, "end") + +t.end()