2
0
mirror of https://gitlab.isc.org/isc-projects/bind9 synced 2025-08-29 13:38:26 +00:00

Implement 'rndc dnssec -checkds'

Add a new 'rndc' command 'dnssec -checkds' that allows the user to
signal named that a new DS record has been seen published in the
parent, or that an existing DS record has been withdrawn from the
parent.

Upon the 'checkds' request, 'named' will write out the new state for
the key, updating the 'DSPublish' or 'DSRemoved' timing metadata.

This replaces the "parent-registration-delay" configuration option,
this was unreliable because it was purely time based (if the user
did not actually submit the new DS to the parent for example, this
could result in an invalid DNSSEC state).

Because we cannot rely on the parent registration delay for state
transition, we need to replace it with a different guard. Instead,
if a key wants its DS state to be moved to RUMOURED, the "DSPublish"
time must be set and must not be in the future. If a key wants its
DS state to be moved to UNRETENTIVE, the "DSRemoved" time must be set
and must not be in the future.

By default, with '-checkds' you set the time that the DS has been
published or withdrawn to now, but you can set a different time with
'-when'. If there is only one KSK for the zone, that key has its
DS state moved to RUMOURED. If there are multiple keys for the zone,
specify the right key with '-key'.
This commit is contained in:
Matthijs Mekking 2020-07-31 08:37:51 +02:00
parent f0fa6f0245
commit 04d8fc0143
10 changed files with 344 additions and 62 deletions

View File

@ -1,3 +1,7 @@
5486. [func] Add 'rndc dnssec -checkds' command to tell named
that the DS record has been published in the parent.
[GL #1613]
5485. [placeholder]
5484. [func] Expire the 0 TTL RRSet quickly rather using them for

View File

@ -89,6 +89,7 @@
#include <dns/secalg.h>
#include <dns/soa.h>
#include <dns/stats.h>
#include <dns/time.h>
#include <dns/tkey.h>
#include <dns/tsig.h>
#include <dns/ttl.h>
@ -14497,6 +14498,23 @@ cleanup:
return (result);
}
static inline bool
argcheck(char *cmd, const char *full) {
size_t l;
if (cmd == NULL || cmd[0] != '-') {
return (false);
}
cmd++;
l = strlen(cmd);
if (l > strlen(full) || strncasecmp(cmd, full, l) != 0) {
return (false);
}
return (true);
}
isc_result_t
named_server_dnssec(named_server_t *server, isc_lex_t *lex,
isc_buffer_t **text) {
@ -14505,11 +14523,16 @@ named_server_dnssec(named_server_t *server, isc_lex_t *lex,
dns_kasp_t *kasp = NULL;
dns_dnsseckeylist_t keys;
dns_dnsseckey_t *key;
const char *ptr;
char *ptr;
const char *msg = NULL;
/* variables for -checkds */
bool checkds = false, dspublish = false, use_keyid = false;
dns_keytag_t keyid = 0;
/* variables for -status */
bool status = false;
char output[4096];
isc_stdtime_t now;
isc_time_t timenow;
isc_stdtime_t now, when;
isc_time_t timenow, timewhen;
const char *dir;
/* Skip the command name. */
@ -14524,43 +14547,163 @@ named_server_dnssec(named_server_t *server, isc_lex_t *lex,
return (ISC_R_UNEXPECTEDEND);
}
if (strcasecmp(ptr, "-status") != 0) {
return (DNS_R_SYNTAX);
}
/* Initialize current time and key list. */
TIME_NOW(&timenow);
now = isc_time_seconds(&timenow);
when = now;
ISC_LIST_INIT(keys);
if (strcasecmp(ptr, "-status") == 0) {
status = true;
} else if (strcasecmp(ptr, "-checkds") == 0) {
checkds = true;
/* Check for options */
for (;;) {
ptr = next_token(lex, text);
if (ptr == NULL) {
msg = "Bad format";
CHECK(ISC_R_UNEXPECTEDEND);
}
if (argcheck(ptr, "key")) {
uint16_t id;
ptr = next_token(lex, text);
if (ptr == NULL) {
msg = "No key identifier specified";
CHECK(ISC_R_UNEXPECTEDEND);
}
CHECK(isc_parse_uint16(&id, ptr, 10));
keyid = (dns_keytag_t)id;
use_keyid = true;
continue;
} else if (argcheck(ptr, "when")) {
uint32_t tw;
ptr = next_token(lex, text);
if (ptr == NULL) {
msg = "No time specified";
CHECK(ISC_R_UNEXPECTEDEND);
}
CHECK(dns_time32_fromtext(ptr, &tw));
when = (isc_stdtime_t)tw;
continue;
} else if (ptr[0] == '-') {
msg = "Unknown option";
CHECK(DNS_R_SYNTAX);
} else {
/*
* No arguments provided, so we must be
* parsing "published|withdrawn".
*/
if (strcasecmp(ptr, "publish") == 0) {
dspublish = true;
} else if (strcasecmp(ptr, "withdraw") != 0) {
CHECK(DNS_R_SYNTAX);
}
}
break;
}
} else {
CHECK(DNS_R_SYNTAX);
}
/* Get zone. */
CHECK(zone_from_args(server, lex, NULL, &zone, NULL, text, false));
if (zone == NULL) {
msg = "Zone not found";
CHECK(ISC_R_UNEXPECTEDEND);
}
/* Trailing garbage? */
ptr = next_token(lex, text);
if (ptr != NULL) {
msg = "Too many arguments";
CHECK(DNS_R_SYNTAX);
}
/* Get dnssec-policy. */
kasp = dns_zone_getkasp(zone);
if (kasp == NULL) {
CHECK(putstr(text, "zone does not have dnssec-policy"));
CHECK(putnull(text));
msg = "Zone does not have dnssec-policy";
goto cleanup;
}
/* -status */
TIME_NOW(&timenow);
now = isc_time_seconds(&timenow);
/* Get DNSSEC keys. */
dir = dns_zone_getkeydirectory(zone);
LOCK(&kasp->lock);
result = dns_dnssec_findmatchingkeys(dns_zone_getorigin(zone), dir, now,
dns_zone_getmctx(zone), &keys);
UNLOCK(&kasp->lock);
if (result != ISC_R_SUCCESS && result != ISC_R_NOTFOUND) {
goto cleanup;
}
LOCK(&kasp->lock);
dns_keymgr_status(kasp, &keys, now, &output[0], sizeof(output));
UNLOCK(&kasp->lock);
CHECK(putstr(text, output));
if (status) {
/*
* Output the DNSSEC status of the key and signing policy.
*/
LOCK(&kasp->lock);
dns_keymgr_status(kasp, &keys, now, &output[0], sizeof(output));
UNLOCK(&kasp->lock);
CHECK(putstr(text, output));
} else if (checkds) {
/*
* Mark DS record has been seen, so it may move to the
* rumoured state.
*/
char whenbuf[80];
isc_time_set(&timewhen, when, 0);
isc_time_formattimestamp(&timewhen, whenbuf, sizeof(whenbuf));
LOCK(&kasp->lock);
if (use_keyid) {
result = dns_keymgr_checkds_id(kasp, &keys, dir, when,
dspublish, keyid);
} else {
result = dns_keymgr_checkds(kasp, &keys, dir, when,
dspublish);
}
UNLOCK(&kasp->lock);
switch (result) {
case ISC_R_SUCCESS:
if (use_keyid) {
char tagbuf[6];
snprintf(tagbuf, sizeof(tagbuf), "%u", keyid);
CHECK(putstr(text, "KSK "));
CHECK(putstr(text, tagbuf));
CHECK(putstr(text, ": "));
}
CHECK(putstr(text, "Marked DS as "));
if (dspublish) {
CHECK(putstr(text, "published "));
} else {
CHECK(putstr(text, "withdrawn "));
}
CHECK(putstr(text, "since "));
CHECK(putstr(text, whenbuf));
break;
case ISC_R_NOTFOUND:
CHECK(putstr(text, "No matching KSK found"));
break;
case ISC_R_FAILURE:
CHECK(putstr(text,
"Error: multiple possible KSKs found, "
"retry command with -key id"));
break;
default:
CHECK(putstr(text, "Error executing checkds command"));
break;
}
}
CHECK(putnull(text));
cleanup:
if (msg != NULL) {
(void)putstr(text, msg);
(void)putnull(text);
}
while (!ISC_LIST_EMPTY(keys)) {
key = ISC_LIST_HEAD(keys);
ISC_LIST_UNLINK(keys, key, link);
@ -14905,23 +15048,6 @@ cleanup:
return (result);
}
static inline bool
argcheck(char *cmd, const char *full) {
size_t l;
if (cmd == NULL || cmd[0] != '-') {
return (false);
}
cmd++;
l = strlen(cmd);
if (l > strlen(full) || strncasecmp(cmd, full, l) != 0) {
return (false);
}
return (true);
}
isc_result_t
named_server_nta(named_server_t *server, isc_lex_t *lex, bool readonly,
isc_buffer_t **text) {

View File

@ -108,6 +108,11 @@ command is one of the following:\n\
Add zone to given view. Requires allow-new-zones option.\n\
delzone [-clean] zone [class [view]]\n\
Removes zone from given view.\n\
dnssec -checkds [-key id] [-when time] (published|withdrawn) zone [class [view]]\n\
Mark the DS record for the KSK of the given zone as seen\n\
in the parent. If the zone has multiple KSKs, select a\n\
specific key by providing the keytag with -key id.\n\
Requires the zone to have a dnssec-policy.\n\
dnssec -status zone [class [view]]\n\
Show the DNSSEC signing state for the specified zone.\n\
Requires the zone to have a dnssec-policy.\n\

View File

@ -161,9 +161,20 @@ Currently supported commands are:
See also ``rndc addzone`` and ``rndc modzone``.
``dnssec`` [**-status** *zone* [*class* [*view*]]
Show the DNSSEC signing state for the specified zone. Requires the
zone to have a "dnssec-policy".
``dnssec`` ( **-status** | **-checkds** [**-key** *id*] [**-when** *time*] ( *published* | *withdrawn* )) *zone* [*class* [*view*]]
This command allows you to interact with the "dnssec-policy" of a given
zone.
``rndc dnssec -status`` show the DNSSEC signing state for the specified
zone.
``rndc dnssec -checkds`` will let ``named`` know that the DS for the given
key has been seen published into or withdrawn from the parent. This is
required in order to complete a KSK rollover. If the ``-key id`` argument
is specified, look for the key with the given identifier, otherwise if there
is only one key acting as a KSK in the zone, assume the DS of that key.
The time that the DS has been published or withdrawn is set to now, unless
otherwise specified with the argument ``-when time``.
``dnstap`` ( **-reopen** | **-roll** [*number*] )
This command closes and re-opens DNSTAP output files. ``rndc dnstap -reopen`` allows

View File

@ -161,9 +161,20 @@ recreated. To remove it permanently, it must also be removed from
.sp
See also \fBrndc addzone\fP and \fBrndc modzone\fP\&.
.TP
\fBdnssec\fP [\fB\-status\fP \fIzone\fP [\fIclass\fP [\fIview\fP]]
Show the DNSSEC signing state for the specified zone. Requires the
zone to have a "dnssec\-policy".
\fBdnssec\fP ( \fB\-status\fP | \fB\-checkds\fP [\fB\-key\fP \fIid\fP] [\fB\-when\fP \fItime\fP] ( \fIpublished\fP | \fIwithdrawn\fP )) \fIzone\fP [\fIclass\fP [\fIview\fP]]
This command allows you to interact with the "dnssec\-policy" of a given
zone.
.sp
\fBrndc dnssec \-status\fP show the DNSSEC signing state for the specified
zone.
.sp
\fBrndc dnssec \-checkds\fP will let \fBnamed\fP know that the DS for the given
key has been seen published into or withdrawn from the parent. This is
required in order to complete a KSK rollover. If the \fB\-key id\fP argument
is specified, look for the key with the given identifier, otherwise if there
is only one key acting as a KSK in the zone, assume the DS of that key.
The time that the DS has been published or withdrawn is set to now, unless
otherwise specified with the argument \fB\-when time\fP\&.
.TP
\fBdnstap\fP ( \fB\-reopen\fP | \fB\-roll\fP [\fInumber\fP] )
This command closes and re\-opens DNSTAP output files. \fBrndc dnstap \-reopen\fP allows

View File

@ -110,7 +110,9 @@ static const char *timingtags[TIMING_NTAGS] = {
"DSPublish:", "SyncPublish:", "SyncDelete:",
"DNSKEYChange:", "ZRRSIGChange:", "KRRSIGChange:", "DSChange:"
"DNSKEYChange:", "ZRRSIGChange:", "KRRSIGChange:", "DSChange:",
"DSRemoved:"
};
#define KEYSTATES_NTAGS (DST_MAX_KEYSTATES + 1)
@ -2009,6 +2011,8 @@ write_key_state(const dst_key_t *key, int type, const char *directory) {
printtime(key, DST_TIME_INACTIVE, "Retired", fp);
printtime(key, DST_TIME_REVOKE, "Revoked", fp);
printtime(key, DST_TIME_DELETE, "Removed", fp);
printtime(key, DST_TIME_DSPUBLISH, "DSPublish", fp);
printtime(key, DST_TIME_DSDELETE, "DSRemoved", fp);
printtime(key, DST_TIME_SYNCPUBLISH, "PublishCDS", fp);
printtime(key, DST_TIME_SYNCDELETE, "DeleteCDS", fp);

View File

@ -51,6 +51,31 @@ dns_keymgr_run(const dns_name_t *origin, dns_rdataclass_t rdclass,
*\li On error, keypool is unchanged
*/
isc_result_t
dns_keymgr_checkds(dns_kasp_t *kasp, dns_dnsseckeylist_t *keyring,
const char *directory, isc_stdtime_t now, bool dspublish);
isc_result_t
dns_keymgr_checkds_id(dns_kasp_t *kasp, dns_dnsseckeylist_t *keyring,
const char *directory, isc_stdtime_t now, bool dspublish,
dns_keytag_t id);
/*%<
* Check DS for one key in 'keyring'. The key must have the KSK role.
* If 'dspublish' is set to true, set the DS Publish time to 'now'.
* If 'dspublish' is set to false, set the DS Removed time to 'now'.
* If a specific key 'id' is given it must match the keytag.
* The result is stored in the key state file.
*
* Requires:
*\li 'kasp' is not NULL.
*\li 'keyring' is not NULL.
*
* Returns:
*\li #ISC_R_SUCCESS (No error).
*\li #ISC_R_FAILURE (More than one matching KSK found).
*\li #ISC_R_NOTFOUND (No matching KSK found).
*
*/
void
dns_keymgr_status(dns_kasp_t *kasp, dns_dnsseckeylist_t *keyring,
isc_stdtime_t now, char *out, size_t out_len);

View File

@ -133,7 +133,8 @@ typedef enum dst_key_state {
#define DST_TIME_ZRRSIG 10
#define DST_TIME_KRRSIG 11
#define DST_TIME_DS 12
#define DST_MAX_TIMES 12
#define DST_TIME_DSDELETE 13
#define DST_MAX_TIMES 13
/* Numeric metadata definitions */
#define DST_NUM_PREDECESSOR 0

View File

@ -128,9 +128,6 @@ keymgr_settime_remove(dns_dnsseckey_t *key, dns_kasp_t *kasp) {
dns_kasp_parentpropagationdelay(kasp) +
dns_kasp_retiresafety(kasp);
}
if (zsk && ksk) {
ksk_remove += dns_kasp_parentregistrationdelay(kasp);
}
remove = ksk_remove > zsk_remove ? ksk_remove : zsk_remove;
dst_key_settime(key->key, DST_TIME_DELETE, remove);
@ -263,12 +260,6 @@ keymgr_prepublication_time(dns_dnsseckey_t *key, dns_kasp_t *kasp,
* so ignore the result code.
*/
(void)dst_key_getbool(key->key, DST_BOOL_ZSK, &zsk);
if (!zsk && ksk) {
/*
* Include registration delay in prepublication time.
*/
prepub += dns_kasp_parentregistrationdelay(kasp);
}
ret = dst_key_gettime(key->key, DST_TIME_INACTIVE, &retire);
if (ret != ISC_R_SUCCESS) {
@ -965,10 +956,14 @@ keymgr_have_rrsig(dns_dnsseckeylist_t *keyring, dns_dnsseckey_t *key, int type,
* - First introduce the DNSKEY record, as well as the KRRSIG records.
* - Only if the DNSKEY record is OMNIPRESENT, suggest to introduce the DS.
*
* Also check the DS Publish or Delete times, to see if the DS record
* already reached the parent.
*/
static bool
keymgr_policy_approval(dns_dnsseckeylist_t *keyring, dns_dnsseckey_t *key,
int type, dst_key_state_t next) {
int type, dst_key_state_t next, isc_stdtime_t now) {
isc_result_t ret;
isc_stdtime_t dstime;
dst_key_state_t dnskeystate = HIDDEN;
dst_key_state_t ksk_present[4] = { OMNIPRESENT, NA, OMNIPRESENT,
OMNIPRESENT };
@ -980,10 +975,10 @@ keymgr_policy_approval(dns_dnsseckeylist_t *keyring, dns_dnsseckey_t *key,
dst_key_state_t ksk_retired[4] = { UNRETENTIVE, NA, NA, OMNIPRESENT };
dst_key_state_t na[4] = { NA, NA, NA, NA }; /* successor n/a */
if (next != RUMOURED) {
if (next != RUMOURED && next != UNRETENTIVE) {
/*
* Local policy only adds an extra barrier on transitions to
* the RUMOURED state.
* the RUMOURED and UNRETENTIVE states.
*/
return (true);
}
@ -993,6 +988,9 @@ keymgr_policy_approval(dns_dnsseckeylist_t *keyring, dns_dnsseckey_t *key,
/* No restrictions. */
return (true);
case DST_KEY_ZRRSIG:
if (next != RUMOURED) {
return (true);
}
/* Make sure the DNSKEY record is OMNIPRESENT. */
(void)dst_key_getstate(key->key, DST_KEY_DNSKEY, &dnskeystate);
if (dnskeystate == OMNIPRESENT) {
@ -1013,13 +1011,35 @@ keymgr_policy_approval(dns_dnsseckeylist_t *keyring, dns_dnsseckey_t *key,
keyring, key, type, next, ksk_retired,
ksk_rumoured, true, true)));
case DST_KEY_KRRSIG:
if (next != RUMOURED) {
return (true);
}
/* Only introduce if the DNSKEY is also introduced. */
(void)dst_key_getstate(key->key, DST_KEY_DNSKEY, &dnskeystate);
return (dnskeystate != HIDDEN);
case DST_KEY_DS:
/* Make sure the DNSKEY record is OMNIPRESENT. */
(void)dst_key_getstate(key->key, DST_KEY_DNSKEY, &dnskeystate);
return (dnskeystate == OMNIPRESENT);
if (next == RUMOURED) {
/* Make sure the DNSKEY record is OMNIPRESENT. */
(void)dst_key_getstate(key->key, DST_KEY_DNSKEY,
&dnskeystate);
if (dnskeystate != OMNIPRESENT) {
return (false);
}
/* Make sure DS has been seen in the parent. */
ret = dst_key_gettime(key->key, DST_TIME_DSPUBLISH,
&dstime);
if (ret != ISC_R_SUCCESS || dstime > now) {
return (false);
}
} else if (next == UNRETENTIVE) {
/* Make sure DS has been withdrawn from the parent. */
ret = dst_key_gettime(key->key, DST_TIME_DSDELETE,
&dstime);
if (ret != ISC_R_SUCCESS || dstime > now) {
return (false);
}
}
return (true);
default:
return (false);
}
@ -1203,16 +1223,13 @@ keymgr_transition_time(dns_dnsseckey_t *key, int type,
*
* Iret = DprpP + TTLds
*
* So we need to wait Dreg + Iret before the DS becomes
* OMNIPRESENT. This translates to:
* This translates to:
*
* parent-registration-delay +
* parent-propagation-delay + parent-ds-ttl.
*
* We will also add the retire-safety interval.
*/
nexttime = lastchange + dns_kasp_dsttl(kasp) +
dns_kasp_parentregistrationdelay(kasp) +
dns_kasp_parentpropagationdelay(kasp) +
dns_kasp_retiresafety(kasp);
break;
@ -1302,7 +1319,7 @@ transition:
/* Is the transition allowed according to policy? */
if (!keymgr_policy_approval(keyring, dkey, i,
next_state)) {
next_state, now)) {
/* No, please respect rollover methods. */
isc_log_write(
dns_lctx, DNS_LOGCATEGORY_DNSSEC,
@ -1433,7 +1450,6 @@ keymgr_key_init(dns_dnsseckey_t *key, dns_kasp_t *kasp, isc_stdtime_t now) {
ret = dst_key_gettime(key->key, DST_TIME_SYNCPUBLISH, &syncpub);
if (syncpub <= now && ret == ISC_R_SUCCESS) {
dns_ttl_t ds_ttl = dns_kasp_dsttl(kasp);
ds_ttl += dns_kasp_parentregistrationdelay(kasp);
ds_ttl += dns_kasp_parentpropagationdelay(kasp);
if ((syncpub + ds_ttl) <= now) {
ds_state = OMNIPRESENT;
@ -1854,6 +1870,83 @@ failure:
return (result);
}
static isc_result_t
keymgr_checkds(dns_kasp_t *kasp, dns_dnsseckeylist_t *keyring,
const char *directory, isc_stdtime_t now, bool dspublish,
dns_keytag_t id, bool check_id) {
int options = (DST_TYPE_PRIVATE | DST_TYPE_PUBLIC | DST_TYPE_STATE);
isc_dir_t dir;
isc_result_t result;
dns_dnsseckey_t *ksk_key = NULL;
REQUIRE(DNS_KASP_VALID(kasp));
REQUIRE(keyring != NULL);
for (dns_dnsseckey_t *dkey = ISC_LIST_HEAD(*keyring); dkey != NULL;
dkey = ISC_LIST_NEXT(dkey, link))
{
isc_result_t ret;
bool ksk = false;
ret = dst_key_getbool(dkey->key, DST_BOOL_KSK, &ksk);
if (ret == ISC_R_SUCCESS && ksk) {
if (check_id && dst_key_id(dkey->key) != id) {
continue;
}
if (ksk_key != NULL) {
/*
* Only checkds for one key at a time.
*/
return (ISC_R_FAILURE);
}
ksk_key = dkey;
}
}
if (ksk_key == NULL) {
return (ISC_R_NOTFOUND);
}
if (dspublish) {
dst_key_settime(ksk_key->key, DST_TIME_DSPUBLISH, now);
} else {
dst_key_settime(ksk_key->key, DST_TIME_DSDELETE, now);
}
/* Store key state and update hints. */
isc_dir_init(&dir);
if (directory == NULL) {
directory = ".";
}
result = isc_dir_open(&dir, directory);
if (result != ISC_R_SUCCESS) {
return result;
}
dns_dnssec_get_hints(ksk_key, now);
result = dst_key_tofile(ksk_key->key, options, directory);
isc_dir_close(&dir);
return (result);
}
isc_result_t
dns_keymgr_checkds(dns_kasp_t *kasp, dns_dnsseckeylist_t *keyring,
const char *directory, isc_stdtime_t now, bool dspublish) {
return (keymgr_checkds(kasp, keyring, directory, now, dspublish, 0,
false));
}
isc_result_t
dns_keymgr_checkds_id(dns_kasp_t *kasp, dns_dnsseckeylist_t *keyring,
const char *directory, isc_stdtime_t now, bool dspublish,
dns_keytag_t id) {
return (keymgr_checkds(kasp, keyring, directory, now, dspublish, id,
true));
}
static void
keytime_status(dst_key_t *key, isc_stdtime_t now, isc_buffer_t *buf,
const char *pre, int ks, int kt) {

View File

@ -470,6 +470,8 @@ dns_kasplist_find
dns_keydata_fromdnskey
dns_keydata_todnskey
dns_keyflags_fromtext
dns_keymgr_checkds
dns_keymgr_checkds_id
dns_keymgr_run
dns_keymgr_status
dns_keynode_dsset