]> git.ipfire.org Git - thirdparty/knot-dns.git/commitdiff
feature/DNSSEC: offline KSK: only with shared kasp db
authorLibor Peltan <libor.peltan@nic.cz>
Fri, 25 May 2018 16:48:38 +0000 (18:48 +0200)
committerLibor Peltan <libor.peltan@nic.cz>
Mon, 5 Nov 2018 09:48:06 +0000 (10:48 +0100)
19 files changed:
Knot.files
doc/man/keymgr.8in
doc/man_keymgr.rst
src/knot/dnssec/context.h
src/knot/dnssec/kasp/kasp_db.c
src/knot/dnssec/kasp/kasp_db.h
src/knot/dnssec/key-events.c
src/knot/dnssec/zone-keys.c
src/knot/dnssec/zone-sign.c
src/knot/dnssec/zone-sign.h
src/knot/journal/serialization.c
src/knot/journal/serialization.h
src/utils/Makefile.inc
src/utils/keymgr/functions.c
src/utils/keymgr/functions.h
src/utils/keymgr/main.c
src/utils/keymgr/offline_ksk.c [new file with mode: 0644]
src/utils/keymgr/offline_ksk.h [new file with mode: 0644]
tests-extra/tests/dnssec/offline_ksk/test.py [new file with mode: 0644]

index 767e039eda55207c2dce6975dc8479ed6cf577d1..3f181255cf55b6052ca83aeab9c9d100883b895b 100644 (file)
@@ -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
index 416318358c6f83c7cb79cb64c04396108a628179..c49bf145dd3e0303a79af943a8c65ff57831924a 100644 (file)
@@ -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.
index 5d14c042d2dc8f2e1d62f8067c3dd0227bc01c8e..9f0552a36c78a5c0431cee9ace7dae5bb05921dc 100644 (file)
@@ -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'.
 
index 2d7da371277eeb0ab9a961e9b4ccabe25f9524d1..795c1235fbbe4c4f2fbe59ec78dfe635d6b99d7c 100644 (file)
@@ -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;
 
 /*!
index 2d4235903a91e8540b2292c29a9eb6591903dc94..a342ed96c0e7bc05b0e96bb06294ebceba5037c5 100644 (file)
@@ -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;
+}
index a80c4b458f5367604d6e3aa7d8f6d17febe3c318..c22b5bb3387d13e4cb8303e3e2c3378340f17ead 100644 (file)
@@ -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);
index ee2c247ab94ded2e4a71e50017b33e6a8c1eee1c..2695993c9f9fe9b9d771e19469ba0d864aff9ae4 100644 (file)
@@ -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) {
index 63211808c7d1c8dd89136a44d75f13e93e66b094..4c06804da22cf19dbab3acf3f082d4f5f47e67ff 100644 (file)
@@ -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;
                }
        }
index 4e17274145234085784bc5bda186bb17af2dedd3..3e2095a2929e8a15b0bd422771a5f13e5cf04532 100644 (file)
@@ -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);
index 8796c4d5373f79cc67d23219922623928fba28df..7f833daef26a074d67c878d5e96935b5ee18c811 100644 (file)
@@ -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.
  *
index ae91b2f6ccde7d45838c82ece05f20042eb76a11..d764fe606cc49efa5ae15c3686d990d3fc528fe3 100644 (file)
@@ -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;
+}
index 04afe394b683d84ef532078d10310531f48c30ef..bc1202711d5ee12f872d32a236ea11ee5bdba05f 100644 (file)
@@ -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);
index abb86990061594f232d4c88648921ff556300d0b..1e3f91daa9a3b61febbbdba698a7a82ab169d867 100644 (file)
@@ -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 = \
index b8dbecf7dd335e43c489945e4584c0dcc0302539..fd9ecf5f06ab91be0dc77fb82c5bb73a224b38dc 100644 (file)
@@ -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);
-}
index 2fbdc56f11274918244b6b02b9e2aaab1a9bddf9..dda367f9399d95708ec534e849dcc38acb81133b 100644 (file)
@@ -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);
index 47f08e7f223e692de3fde300974fe450e512c616..cd629deaa60baac913f7d433c349bc3c6a3ae21c 100644 (file)
@@ -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 (file)
index 0000000..cf1e92a
--- /dev/null
@@ -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 (file)
index 0000000..37c1fab
--- /dev/null
@@ -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 (file)
index 0000000..1ac056b
--- /dev/null
@@ -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()