diff --git a/doc/man/knot.conf.5in b/doc/man/knot.conf.5in index 2f88f28c6..607e6cc73 100644 --- a/doc/man/knot.conf.5in +++ b/doc/man/knot.conf.5in @@ -1869,6 +1869,10 @@ A period how long at least before a signature expiration the signature will be r in order to prevent expired RRSIGs on secondary servers or resolvers\(aq caches. .sp \fIDefault:\fP 0.1 * \fI\%rrsig\-lifetime\fP + \fI\%propagation\-delay\fP + \fI\%zone\-max\-ttl\fP +.sp +If \fI\%dnssec\-validation\fP is enabled: +.sp +\fIDefault:\fP \fB1d\fP (1 day) .SS rrsig\-pre\-refresh .sp A period how long at most before a signature refresh time the signature might be refreshed, @@ -2522,7 +2526,9 @@ Every NSEC(3) RR is linked to the lexicographically next one. .sp The validation is not affected by \fI\%dnssec\-policy\fP configuration, except for \fI\%signing\-threads\fP option, which specifies the number -of threads for parallel validation. +of threads for parallel validation, and \fI\%rrsig\-refresh\fP, which +defines minimal allowed remaining RRSIG validity (otherwise a warning is +logged). .sp \fBNOTE:\fP .INDENT 0.0 diff --git a/doc/reference.rst b/doc/reference.rst index 6bc70ae9c..53bdae28a 100644 --- a/doc/reference.rst +++ b/doc/reference.rst @@ -2056,6 +2056,10 @@ in order to prevent expired RRSIGs on secondary servers or resolvers' caches. *Default:* 0.1 * :ref:`policy_rrsig-lifetime` + :ref:`policy_propagation-delay` + :ref:`policy_zone-max-ttl` +If :ref:`zone_dnssec-validation` is enabled: + +*Default:* ``1d`` (1 day) + .. _policy_rrsig-pre-refresh: rrsig-pre-refresh @@ -2744,7 +2748,9 @@ List of DNSSEC checks: The validation is not affected by :ref:`zone_dnssec-policy` configuration, except for :ref:`policy_signing-threads` option, which specifies the number -of threads for parallel validation. +of threads for parallel validation, and :ref:`policy_rrsig-refresh`, which +defines minimal allowed remaining RRSIG validity (otherwise a warning is +logged). .. NOTE:: diff --git a/src/contrib/time.h b/src/contrib/time.h index 20d241e3c..b12b3662d 100644 --- a/src/contrib/time.h +++ b/src/contrib/time.h @@ -88,6 +88,11 @@ inline static int knot_time_cmp(knot_time_t a, knot_time_t b) { return (a == b ? 0 : 1) * ((a && b) == 0 ? -1 : 1) * (a < b ? -1 : 1); } +inline static bool knot_time_lt (knot_time_t a, knot_time_t b) { return knot_time_cmp(a, b) < 0; } +inline static bool knot_time_leq(knot_time_t a, knot_time_t b) { return knot_time_cmp(a, b) <= 0; } +inline static bool knot_time_eq (knot_time_t a, knot_time_t b) { return knot_time_cmp(a, b) == 0; } +inline static bool knot_time_geq(knot_time_t a, knot_time_t b) { return knot_time_cmp(a, b) >= 0; } +inline static bool knot_time_gt (knot_time_t a, knot_time_t b) { return knot_time_cmp(a, b) > 0; } /*! * \brief Return the smaller (=earlier) from given two timestamps. diff --git a/src/knot/conf/schema.c b/src/knot/conf/schema.c index ff57e5cf9..3469c934f 100644 --- a/src/knot/conf/schema.c +++ b/src/knot/conf/schema.c @@ -418,7 +418,7 @@ static const yp_item_t desc_policy[] = { CONF_IO_FRLD_ZONES }, { C_RRSIG_REFRESH, YP_TINT, YP_VINT = { 1, INT32_MAX, YP_NIL, YP_STIME }, CONF_IO_FRLD_ZONES }, - { C_RRSIG_PREREFRESH, YP_TINT, YP_VINT = { 0, INT32_MAX, HOURS(1), YP_STIME }, + { C_RRSIG_PREREFRESH, YP_TINT, YP_VINT = { 0, INT32_MAX, HOURS(1), YP_STIME, DAYS(1) }, CONF_IO_FRLD_ZONES }, { C_REPRO_SIGNING, YP_TBOOL, YP_VNONE, CONF_IO_FRLD_ZONES }, { C_NSEC3, YP_TBOOL, YP_VNONE, CONF_IO_FRLD_ZONES }, diff --git a/src/knot/dnssec/context.c b/src/knot/dnssec/context.c index 679509b6d..d268689ea 100644 --- a/src/knot/dnssec/context.c +++ b/src/knot/dnssec/context.c @@ -338,8 +338,10 @@ int kdnssec_validation_ctx(conf_t *conf, kdnssec_ctx_t *ctx, const zone_contents if (conf != NULL) { conf_val_t policy_id = conf_zone_get(conf, C_DNSSEC_POLICY, zone->apex->owner); conf_id_fix_default(&policy_id); - conf_val_t num_threads = conf_id_get(conf, C_POLICY, C_SIGNING_THREADS, &policy_id); - ctx->policy->signing_threads = conf_int(&num_threads); + conf_val_t val = conf_id_get(conf, C_POLICY, C_SIGNING_THREADS, &policy_id); + ctx->policy->signing_threads = conf_int(&val); + val = conf_id_get(conf, C_POLICY, C_RRSIG_REFRESH, &policy_id); + ctx->policy->rrsig_refresh_before = conf_int_alt(&val, true); } else { ctx->policy->signing_threads = MAX(dt_optimal_size(), 1); } diff --git a/src/knot/dnssec/key_records.c b/src/knot/dnssec/key_records.c index 80a9179fd..366ab4a21 100644 --- a/src/knot/dnssec/key_records.c +++ b/src/knot/dnssec/key_records.c @@ -211,7 +211,7 @@ int key_records_sign(const zone_key_t *key, key_records_t *r, const kdnssec_ctx_ return ret; } -int key_records_verify(key_records_t *r, kdnssec_ctx_t *kctx, knot_time_t timestamp) +int key_records_verify(key_records_t *r, kdnssec_ctx_t *kctx, knot_time_t timestamp, knot_time_t min_valid) { kctx->now = timestamp; int ret = kasp_zone_keys_from_rr(kctx->zone, &r->dnskey.rrs, false, &kctx->keytag_conflict); @@ -224,12 +224,17 @@ int key_records_verify(key_records_t *r, kdnssec_ctx_t *kctx, knot_time_t timest return KNOT_ENOMEM; } - ret = knot_validate_rrsigs(&r->dnskey, &r->rrsig, sign_ctx, false); + knot_time_t until = 0; + ret = knot_validate_rrsigs(&r->dnskey, &r->rrsig, sign_ctx, false, &until); if (ret == KNOT_EOK && !knot_rrset_empty(&r->cdnskey)) { - ret = knot_validate_rrsigs(&r->cdnskey, &r->rrsig, sign_ctx, false); + ret = knot_validate_rrsigs(&r->cdnskey, &r->rrsig, sign_ctx, false, &until); } if (ret == KNOT_EOK && !knot_rrset_empty(&r->cds)) { - ret = knot_validate_rrsigs(&r->cds, &r->rrsig, sign_ctx, false); + ret = knot_validate_rrsigs(&r->cds, &r->rrsig, sign_ctx, false, &until); + } + + if (ret == KNOT_EOK && knot_time_lt(until, min_valid)) { + ret = KNOT_ESOON_EXPIRE; } zone_sign_ctx_free(sign_ctx); diff --git a/src/knot/dnssec/key_records.h b/src/knot/dnssec/key_records.h index b02964252..dd28b4fb4 100644 --- a/src/knot/dnssec/key_records.h +++ b/src/knot/dnssec/key_records.h @@ -42,7 +42,7 @@ int key_records_dump(char **buf, size_t *buf_size, const key_records_t *r, bool int key_records_sign(const zone_key_t *key, key_records_t *r, const kdnssec_ctx_t *kctx); // WARNING this modifies 'kctx' with updated timestamp and with zone_keys from r->dnskey -int key_records_verify(key_records_t *r, kdnssec_ctx_t *kctx, knot_time_t timestamp); +int key_records_verify(key_records_t *r, kdnssec_ctx_t *kctx, knot_time_t timestamp, knot_time_t min_valid); size_t key_records_serialized_size(const key_records_t *r); diff --git a/src/knot/dnssec/zone-sign.c b/src/knot/dnssec/zone-sign.c index 7ca337fe5..ccd18b689 100644 --- a/src/knot/dnssec/zone-sign.c +++ b/src/knot/dnssec/zone-sign.c @@ -300,9 +300,10 @@ static bool key_used(bool ksk, bool zsk, uint16_t type, int knot_validate_rrsigs(const knot_rrset_t *covered, const knot_rrset_t *rrsigs, zone_sign_ctx_t *sign_ctx, - bool skip_crypto) + bool skip_crypto, + knot_time_t *valid_until) { - if (covered == NULL || rrsigs == NULL || sign_ctx == NULL) { + if (covered == NULL || rrsigs == NULL || sign_ctx == NULL || valid_until == NULL) { return KNOT_EINVAL; } @@ -319,6 +320,8 @@ int knot_validate_rrsigs(const knot_rrset_t *covered, if (valid_signature_exists(covered, rrsigs, key->key, sign_ctx->sign_ctxs[i], sign_ctx->dnssec_ctx, 0, skip_crypto, &ret, &valid_at)) { valid_exists = true; + knot_rdata_t *valid_rr = knot_rdataset_at(&rrsigs->rrs, valid_at); + note_earliest_expiration(valid_rr, sign_ctx->dnssec_ctx->now, valid_until); } knot_spin_lock(&sign_ctx->dnssec_ctx->stats->lock); @@ -483,10 +486,15 @@ static int sign_node_rrsets(const zone_node_t *node, } if (sign_ctx->dnssec_ctx->validation_mode) { - result = knot_validate_rrsigs(&rrset, &rrsigs, sign_ctx, skip_crypto); + knot_time_t until = 0; + result = knot_validate_rrsigs(&rrset, &rrsigs, sign_ctx, skip_crypto, &until); if (result != KNOT_EOK) { hint->node = node->owner; hint->rrtype = rrset.type; + } else if (knot_time_lt(until, sign_ctx->dnssec_ctx->now + sign_ctx->dnssec_ctx->policy->rrsig_refresh_before)) { + hint->node = node->owner; + hint->rrtype = rrset.type; + hint->warning = KNOT_ESOON_EXPIRE; } } else if (sign_ctx->dnssec_ctx->rrsig_drop_existing) { result = force_resign_rrset(&rrset, &rrsigs, diff --git a/src/knot/dnssec/zone-sign.h b/src/knot/dnssec/zone-sign.h index 14212edb7..480bcf9d7 100644 --- a/src/knot/dnssec/zone-sign.h +++ b/src/knot/dnssec/zone-sign.h @@ -81,13 +81,15 @@ keyptr_dynarray_t knot_zone_sign_get_cdnskeys(const kdnssec_ctx_t *ctx, * \param rrsigs RRSIG with signatures. * \param sign_ctx Signing context (with keys == NULL) * \param skip_crypto Crypto operations might be skipped as they had been successful earlier. + * \param valid_until End of soonest RRSIG validity. * * \return KNOT_E* */ int knot_validate_rrsigs(const knot_rrset_t *covered, const knot_rrset_t *rrsigs, zone_sign_ctx_t *sign_ctx, - bool skip_crypto); + bool skip_crypto, + knot_time_t *valid_until); /*! * \brief Update zone signatures and store performed changes in update. diff --git a/src/knot/updates/zone-update.c b/src/knot/updates/zone-update.c index 451c014cf..5882bfff2 100644 --- a/src/knot/updates/zone-update.c +++ b/src/knot/updates/zone-update.c @@ -894,6 +894,23 @@ int zone_update_verify_digest(conf_t *conf, zone_update_t *update) return ret; } +static void log_validation_error(zone_update_t *update, const char *msg_valid, + int ret, bool warning) +{ + unsigned level = warning ? LOG_WARNING : LOG_ERR; + + log_fmt_zone(level, LOG_SOURCE_ZONE, update->zone->name, NULL, + "DNSSEC, %svalidation failed (%s)", msg_valid, knot_strerror(ret)); + + char type_str[16]; + knot_dname_txt_storage_t name_str; + if (knot_dname_to_str(name_str, update->validation_hint.node, sizeof(name_str)) != NULL && + knot_rrtype_to_string(update->validation_hint.rrtype, type_str, sizeof(type_str)) >= 0) { + log_fmt_zone(level, LOG_SOURCE_ZONE, update->zone->name, NULL, + "DNSSEC, validation hint: %s %s", name_str, type_str); + } +} + int zone_update_commit(conf_t *conf, zone_update_t *update) { if (conf == NULL || update == NULL) { @@ -947,20 +964,14 @@ int zone_update_commit(conf_t *conf, zone_update_t *update) size_t count = 0; ret = knot_dnssec_validate_zone(update, conf, 0, incr_valid, &count); if (ret != KNOT_EOK) { - log_zone_error(update->zone->name, "DNSSEC, %svalidation failed (%s)", - msg_valid, knot_strerror(ret)); - char type_str[16]; - knot_dname_txt_storage_t name_str; - if (knot_dname_to_str(name_str, update->validation_hint.node, sizeof(name_str)) != NULL && - knot_rrtype_to_string(update->validation_hint.rrtype, type_str, sizeof(type_str)) >= 0) { - log_zone_error(update->zone->name, "DNSSEC, validation hint: %s %s", - name_str, type_str); - } - discard_adds_tree(update); + log_validation_error(update, msg_valid, ret, false); if (conf->cache.srv_dbus_event & DBUS_EVENT_ZONE_INVALID) { systemd_emit_zone_invalid(update->zone->name); } + discard_adds_tree(update); return ret; + } else if (update->validation_hint.warning != KNOT_EOK) { + log_validation_error(update, msg_valid, update->validation_hint.warning, true); } else { log_zone_info(update->zone->name, "DNSSEC, %svalidation successful, checked RRSIGs %zu", msg_valid, count); diff --git a/src/knot/updates/zone-update.h b/src/knot/updates/zone-update.h index 0499d72c2..edf85d858 100644 --- a/src/knot/updates/zone-update.h +++ b/src/knot/updates/zone-update.h @@ -26,6 +26,7 @@ typedef struct { knot_dname_storage_t next; const knot_dname_t *node; uint16_t rrtype; + int warning; } dnssec_validation_hint_t; /*! \brief Structure for zone contents updating / querying. */ diff --git a/src/libknot/errcode.h b/src/libknot/errcode.h index 2c588f047..6903ee620 100644 --- a/src/libknot/errcode.h +++ b/src/libknot/errcode.h @@ -173,6 +173,7 @@ enum knot_error { KNOT_NO_PUBLIC_KEY, KNOT_NO_PRIVATE_KEY, KNOT_NO_READY_KEY, + KNOT_ESOON_EXPIRE, KNOT_ERROR_MAX = -501 }; diff --git a/src/libknot/error.c b/src/libknot/error.c index f368fac6b..8e0cf6fdc 100644 --- a/src/libknot/error.c +++ b/src/libknot/error.c @@ -172,6 +172,7 @@ static const struct error errors[] = { { KNOT_NO_PUBLIC_KEY, "no public key" }, { KNOT_NO_PRIVATE_KEY, "no private key" }, { KNOT_NO_READY_KEY, "no key ready for submission" }, + { KNOT_ESOON_EXPIRE, "oncoming RRSIG expiration" }, /* Terminator */ { KNOT_ERROR, NULL } diff --git a/src/utils/keymgr/offline_ksk.c b/src/utils/keymgr/offline_ksk.c index 3e2da0087..ab13d5199 100644 --- a/src/utils/keymgr/offline_ksk.c +++ b/src/utils/keymgr/offline_ksk.c @@ -451,6 +451,7 @@ static void skr_import_header(zs_scanner_t *sc) // trailing header without timestamp next_timestamp = 0; } + knot_time_t validity_ts = next_timestamp != 0 ? next_timestamp : ctx->timestamp; // delete possibly existing conflicting offline records ctx->ret = kasp_db_delete_offline_records( @@ -459,7 +460,7 @@ static void skr_import_header(zs_scanner_t *sc) // store previous SKR if (ctx->timestamp > 0 && ctx->ret == KNOT_EOK) { - ctx->ret = key_records_verify(&ctx->r, ctx->kctx, ctx->timestamp); + ctx->ret = key_records_verify(&ctx->r, ctx->kctx, ctx->timestamp, validity_ts); if (ctx->ret != KNOT_EOK) { return; } @@ -490,9 +491,10 @@ static void skr_validate_header(zs_scanner_t *sc) // trailing header without timestamp next_timestamp = 0; } + knot_time_t validity_ts = next_timestamp != 0 ? next_timestamp : ctx->timestamp; if (ctx->timestamp > 0 && ctx->ret == KNOT_EOK) { - int ret = key_records_verify(&ctx->r, ctx->kctx, ctx->timestamp); + int ret = key_records_verify(&ctx->r, ctx->kctx, ctx->timestamp, validity_ts); if (ret != KNOT_EOK) { // ctx->ret untouched ERR2("invalid SignedKeyResponse for %"KNOT_TIME_PRINTF" (%s)", ctx->timestamp, knot_strerror(ret)); diff --git a/tests-extra/tools/dnstest/server.py b/tests-extra/tools/dnstest/server.py index da700c5b6..24faa53c2 100644 --- a/tests-extra/tools/dnstest/server.py +++ b/tests-extra/tools/dnstest/server.py @@ -1570,7 +1570,7 @@ class Knot(Server): have_policy = False for zone in sorted(self.zones): z = self.zones[zone] - if not z.dnssec.enable: + if not z.dnssec.enable and not z.dnssec.validate: continue if (z.dnssec.shared_policy_with or z.name) != z.name: @@ -1737,6 +1737,8 @@ class Knot(Server): if z.dnssec.enable: s.item_str("dnssec-signing", "off" if z.dnssec.disable else "on") + + if z.dnssec.enable or z.dnssec.validate: s.item_str("dnssec-policy", z.dnssec.shared_policy_with or z.name) self._bool(s, "dnssec-validation", z.dnssec.validate)