dnssec/validation: consider end of RRSIG validitiy...

...for dnssec-validate that it is longer than rrsig-refresh
...for keymgr offline-ksk, that it's until the next DNSKEY snapshot
This commit is contained in:
Libor Peltan 2023-12-11 17:45:43 +01:00 committed by Daniel Salzman
parent de06303cbc
commit ce1e335c95
15 changed files with 79 additions and 27 deletions

View file

@ -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

View file

@ -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::

View file

@ -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.

View file

@ -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 },

View file

@ -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);
}

View file

@ -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);

View file

@ -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);

View file

@ -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,

View file

@ -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.

View file

@ -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);

View file

@ -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. */

View file

@ -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
};

View file

@ -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 }

View file

@ -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));

View file

@ -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)