diff --git a/CHANGES b/CHANGES index b01a399729..6aa1d6be82 100644 --- a/CHANGES +++ b/CHANGES @@ -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 diff --git a/bin/named/server.c b/bin/named/server.c index 88726bb4c7..89f8eaa29b 100644 --- a/bin/named/server.c +++ b/bin/named/server.c @@ -89,6 +89,7 @@ #include #include #include +#include #include #include #include @@ -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) { diff --git a/bin/rndc/rndc.c b/bin/rndc/rndc.c index ef6b31c531..e68c2e2244 100644 --- a/bin/rndc/rndc.c +++ b/bin/rndc/rndc.c @@ -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\ diff --git a/bin/rndc/rndc.rst b/bin/rndc/rndc.rst index 163786571d..72e42384cb 100644 --- a/bin/rndc/rndc.rst +++ b/bin/rndc/rndc.rst @@ -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 diff --git a/doc/man/rndc.8in b/doc/man/rndc.8in index c1988be64b..81fd8f642b 100644 --- a/doc/man/rndc.8in +++ b/doc/man/rndc.8in @@ -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 diff --git a/lib/dns/dst_api.c b/lib/dns/dst_api.c index 7d7940a851..f9cf22ec5d 100644 --- a/lib/dns/dst_api.c +++ b/lib/dns/dst_api.c @@ -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); diff --git a/lib/dns/include/dns/keymgr.h b/lib/dns/include/dns/keymgr.h index df48b792bf..3e8ad6af30 100644 --- a/lib/dns/include/dns/keymgr.h +++ b/lib/dns/include/dns/keymgr.h @@ -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); diff --git a/lib/dns/include/dst/dst.h b/lib/dns/include/dst/dst.h index e4ac373709..bdf95522f6 100644 --- a/lib/dns/include/dst/dst.h +++ b/lib/dns/include/dst/dst.h @@ -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 diff --git a/lib/dns/keymgr.c b/lib/dns/keymgr.c index 23dc33d74c..c8a5badb78 100644 --- a/lib/dns/keymgr.c +++ b/lib/dns/keymgr.c @@ -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) { diff --git a/lib/dns/win32/libdns.def.in b/lib/dns/win32/libdns.def.in index be5f94c7f5..e08904ff8c 100644 --- a/lib/dns/win32/libdns.def.in +++ b/lib/dns/win32/libdns.def.in @@ -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