]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Add checkds code
authorMatthijs Mekking <matthijs@isc.org>
Thu, 24 Jun 2021 15:01:42 +0000 (17:01 +0200)
committerMatthijs Mekking <matthijs@isc.org>
Wed, 30 Jun 2021 15:28:49 +0000 (17:28 +0200)
Similar to notify, add code to send and keep track of checkds requests.

On every zone_rekey event, we will check the DS at parental agents
(but we will only actually query parental agents if theree is a DS
scheduled to be published/withdrawn).

On a zone_rekey event, we will first clear the ongoing checkds requests.
Reset the counter, to avoid continuing KSK rollover premature.

This has the risk that if zone_rekey events happen too soon after each
other, there are redundant DS queries to the parental agents. But
if TTLs and the configured durations in the dnssec-policy are sane (as
in not ridiculous short) the chance of this happening is low.

lib/dns/include/dns/events.h
lib/dns/zone.c

index 894c4dd211385587a601563676e2dda7badc5b46..7d93637268f5dcef4e678f5b0db3eaf39933d7e5 100644 (file)
@@ -80,6 +80,7 @@
 #define DNS_EVENT_STARTUPDATE       (ISC_EVENTCLASS_DNS + 58)
 #define DNS_EVENT_TRYSTALE          (ISC_EVENTCLASS_DNS + 59)
 #define DNS_EVENT_ZONEFLUSH         (ISC_EVENTCLASS_DNS + 60)
+#define DNS_EVENT_CHECKDSSENDTOADDR  (ISC_EVENTCLASS_DNS + 61)
 
 #define DNS_EVENT_FIRSTEVENT (ISC_EVENTCLASS_DNS + 0)
 #define DNS_EVENT_LASTEVENT  (ISC_EVENTCLASS_DNS + 65535)
index 1619b60d55d450a0c190971ef4bce602291d9261..e850e5de6e1b3c800454037a55b74422e6a1e8f4 100644 (file)
@@ -93,6 +93,9 @@
 #define NOTIFY_MAGIC            ISC_MAGIC('N', 't', 'f', 'y')
 #define DNS_NOTIFY_VALID(notify) ISC_MAGIC_VALID(notify, NOTIFY_MAGIC)
 
+#define CHECKDS_MAGIC             ISC_MAGIC('C', 'h', 'D', 'S')
+#define DNS_CHECKDS_VALID(checkds) ISC_MAGIC_VALID(checkds, CHECKDS_MAGIC)
+
 #define STUB_MAGIC          ISC_MAGIC('S', 't', 'u', 'b')
 #define DNS_STUB_VALID(stub) ISC_MAGIC_VALID(stub, STUB_MAGIC)
 
 #endif                    /* ifndef DNS_DUMP_DELAY */
 
 typedef struct dns_notify dns_notify_t;
+typedef struct dns_checkds dns_checkds_t;
 typedef struct dns_stub dns_stub_t;
 typedef struct dns_load dns_load_t;
 typedef struct dns_forward dns_forward_t;
@@ -330,6 +334,7 @@ struct dns_zone {
        bool zero_no_soa_ttl;
        dns_severity_t check_names;
        ISC_LIST(dns_notify_t) notifies;
+       ISC_LIST(dns_checkds_t) checkds_requests;
        dns_request_t *request;
        dns_loadctx_t *lctx;
        dns_io_t *readio;
@@ -646,6 +651,23 @@ struct dns_notify {
 #define DNS_NOTIFY_NOSOA   0x0001U
 #define DNS_NOTIFY_STARTUP 0x0002U
 
+/*%
+ * Hold checkds state.
+ */
+struct dns_checkds {
+       unsigned int magic;
+       unsigned int flags;
+       isc_mem_t *mctx;
+       dns_zone_t *zone;
+       dns_request_t *request;
+       isc_sockaddr_t dst;
+       dns_tsigkey_t *key;
+       dns_transport_t *transport;
+       isc_dscp_t dscp;
+       ISC_LINK(dns_checkds_t) link;
+       isc_event_t *event;
+};
+
 /*%
  *     dns_stub holds state while performing a 'stub' transfer.
  *     'db' is the zone's 'db' or a new one if this is the initial
@@ -867,6 +889,16 @@ ns_query(dns_zone_t *zone, dns_rdataset_t *soardataset, dns_stub_t *stub);
 static int
 message_count(dns_message_t *msg, dns_section_t section, dns_rdatatype_t type);
 static void
+checkds_cancel(dns_zone_t *zone);
+static void
+checkds_send(dns_zone_t *zone);
+static isc_result_t
+checkds_createmessage(dns_zone_t *zone, dns_message_t **messagep);
+static void
+checkds_done(isc_task_t *task, isc_event_t *event);
+static void
+checkds_send_toaddr(isc_task_t *task, isc_event_t *event);
+static void
 notify_cancel(dns_zone_t *zone);
 static void
 notify_find_address(dns_notify_t *notify);
@@ -1106,6 +1138,7 @@ dns_zone_create(dns_zone_t **zonep, isc_mem_t *mctx) {
        isc_time_settoepoch(&zone->nsec3chaintime);
        isc_time_settoepoch(&zone->refreshkeytime);
        ISC_LIST_INIT(zone->notifies);
+       ISC_LIST_INIT(zone->checkds_requests);
        isc_sockaddr_any(&zone->notifysrc4);
        isc_sockaddr_any6(&zone->notifysrc6);
        isc_sockaddr_any(&zone->parentalsrc4);
@@ -5957,7 +5990,6 @@ dns_zone_getaltxfrsource6dscp(dns_zone_t *zone) {
        return (zone->altxfrsource6dscp);
 }
 
-
 isc_result_t
 dns_zone_setparentalsrc4(dns_zone_t *zone, const isc_sockaddr_t *parentalsrc) {
        REQUIRE(DNS_ZONE_VALID(zone));
@@ -12141,6 +12173,25 @@ notify_cancel(dns_zone_t *zone) {
        }
 }
 
+static void
+checkds_cancel(dns_zone_t *zone) {
+       dns_checkds_t *checkds;
+
+       /*
+        * 'zone' locked by caller.
+        */
+
+       REQUIRE(LOCKED_ZONE(zone));
+
+       for (checkds = ISC_LIST_HEAD(zone->checkds_requests); checkds != NULL;
+            checkds = ISC_LIST_NEXT(checkds, link))
+       {
+               if (checkds->request != NULL) {
+                       dns_request_cancel(checkds->request);
+               }
+       }
+}
+
 static void
 forward_cancel(dns_zone_t *zone) {
        dns_forward_t *forward;
@@ -14940,6 +14991,8 @@ zone_shutdown(isc_task_t *task, isc_event_t *event) {
                }
        }
 
+       checkds_cancel(zone);
+
        notify_cancel(zone);
 
        forward_cancel(zone);
@@ -20284,6 +20337,781 @@ dnssec_report(const char *format, ...) {
        va_end(args);
 }
 
+static void
+checkds_destroy(dns_checkds_t *checkds, bool locked) {
+       isc_mem_t *mctx;
+
+       REQUIRE(DNS_CHECKDS_VALID(checkds));
+
+       dns_zone_log(checkds->zone, ISC_LOG_DEBUG(3),
+                    "checkds: destroy DS query");
+
+       if (checkds->zone != NULL) {
+               if (!locked) {
+                       LOCK_ZONE(checkds->zone);
+               }
+               REQUIRE(LOCKED_ZONE(checkds->zone));
+               if (ISC_LINK_LINKED(checkds, link)) {
+                       ISC_LIST_UNLINK(checkds->zone->checkds_requests,
+                                       checkds, link);
+               }
+               if (!locked) {
+                       UNLOCK_ZONE(checkds->zone);
+               }
+               if (locked) {
+                       zone_idetach(&checkds->zone);
+               } else {
+                       dns_zone_idetach(&checkds->zone);
+               }
+       }
+       if (checkds->request != NULL) {
+               dns_request_destroy(&checkds->request);
+       }
+       if (checkds->key != NULL) {
+               dns_tsigkey_detach(&checkds->key);
+       }
+       if (checkds->transport != NULL) {
+               dns_transport_detach(&checkds->transport);
+       }
+       mctx = checkds->mctx;
+       isc_mem_put(checkds->mctx, checkds, sizeof(*checkds));
+       isc_mem_detach(&mctx);
+}
+
+static isc_result_t
+make_dnskey(dst_key_t *key, unsigned char *buf, int bufsize,
+           dns_rdata_t *target) {
+       isc_result_t result;
+       isc_buffer_t b;
+       isc_region_t r;
+
+       isc_buffer_init(&b, buf, bufsize);
+       result = dst_key_todns(key, &b);
+       if (result != ISC_R_SUCCESS) {
+               return (result);
+       }
+
+       dns_rdata_reset(target);
+       isc_buffer_usedregion(&b, &r);
+       dns_rdata_fromregion(target, dst_key_class(key), dns_rdatatype_dnskey,
+                            &r);
+       return (ISC_R_SUCCESS);
+}
+
+static bool
+do_checkds(dns_zone_t *zone, dst_key_t *key, isc_stdtime_t now,
+          bool dspublish) {
+       dns_kasp_t *kasp = dns_zone_getkasp(zone);
+       const char *dir = dns_zone_getkeydirectory(zone);
+       isc_result_t result;
+       uint32_t count = 0;
+
+       if (dspublish) {
+               (void)dst_key_getnum(key, DST_NUM_DSPUBCOUNT, &count);
+               count += 1;
+               dst_key_setnum(key, DST_NUM_DSPUBCOUNT, count);
+               dns_zone_log(zone, ISC_LOG_DEBUG(3),
+                            "checkds: %u DS published "
+                            "for key %u",
+                            count, dst_key_id(key));
+
+               if (count != zone->parentalscnt) {
+                       return false;
+               }
+       } else {
+               (void)dst_key_getnum(key, DST_NUM_DSDELCOUNT, &count);
+               count += 1;
+               dst_key_setnum(key, DST_NUM_DSDELCOUNT, count);
+               dns_zone_log(zone, ISC_LOG_DEBUG(3),
+                            "checkds: %u DS withdrawn "
+                            "for key %u",
+                            count, dst_key_id(key));
+
+               if (count != zone->parentalscnt) {
+                       return false;
+               }
+       }
+
+       dns_zone_log(zone, ISC_LOG_DEBUG(3),
+                    "checkds: checkds %s for key "
+                    "%u",
+                    dspublish ? "published" : "withdrawn", dst_key_id(key));
+
+       dns_zone_lock_keyfiles(zone);
+       result = dns_keymgr_checkds_id(kasp, &zone->checkds_ok, dir, now, now,
+                                      dspublish, dst_key_id(key),
+                                      dst_key_alg(key));
+       dns_zone_unlock_keyfiles(zone);
+
+       if (result != ISC_R_SUCCESS) {
+               dns_zone_log(zone, ISC_LOG_WARNING,
+                            "checkds: checkds for key %u failed: %s",
+                            dst_key_id(key), isc_result_totext(result));
+               return false;
+       }
+
+       return true;
+}
+
+static isc_result_t
+validate_ds(dns_zone_t *zone, dns_message_t *message) {
+       UNUSED(zone);
+       UNUSED(message);
+
+       /* Get closest trust anchor */
+
+       /* Check that trust anchor is (grand)parent of zone. */
+
+       /* Find the DNSKEY signing the message. */
+
+       /* Check that DNSKEY is in chain of trust. */
+
+       /* Validate DS RRset. */
+
+       return (ISC_R_SUCCESS);
+}
+
+static void
+checkds_done(isc_task_t *task, isc_event_t *event) {
+       char addrbuf[ISC_SOCKADDR_FORMATSIZE];
+       char rcode[128];
+       dns_checkds_t *checkds;
+       dns_zone_t *zone;
+       dns_db_t *db = NULL;
+       dns_dbversion_t *version = NULL;
+       dns_dnsseckey_t *key;
+       dns_dnsseckeylist_t keys;
+       dns_kasp_t *kasp = NULL;
+       dns_message_t *message = NULL;
+       dns_rdataset_t *ds_rrset = NULL;
+       dns_requestevent_t *revent = (dns_requestevent_t *)event;
+       isc_buffer_t buf;
+       isc_result_t result;
+       isc_stdtime_t now;
+       isc_time_t timenow;
+       bool rekey = false;
+       bool empty = false;
+
+       UNUSED(task);
+
+       checkds = event->ev_arg;
+       REQUIRE(DNS_CHECKDS_VALID(checkds));
+
+       zone = checkds->zone;
+       INSIST(task == zone->task);
+
+       ISC_LIST_INIT(keys);
+
+       kasp = zone->kasp;
+       INSIST(kasp != NULL);
+
+       isc_buffer_init(&buf, rcode, sizeof(rcode));
+       isc_sockaddr_format(&checkds->dst, addrbuf, sizeof(addrbuf));
+
+       dns_zone_log(zone, ISC_LOG_DEBUG(1), "checkds: DS query to %s: done",
+                    addrbuf);
+
+       dns_message_create(zone->mctx, DNS_MESSAGE_INTENTPARSE, &message);
+       INSIST(message != NULL);
+
+       CHECK(revent->result);
+       CHECK(dns_request_getresponse(revent->request, message,
+                                     DNS_MESSAGEPARSE_PRESERVEORDER));
+       CHECK(dns_rcode_totext(message->rcode, &buf));
+
+       dns_zone_log(zone, ISC_LOG_DEBUG(3),
+                    "checkds: DS response from %s: %.*s", addrbuf,
+                    (int)buf.used, rcode);
+
+       /* Validate response. */
+       CHECK(validate_ds(zone, message));
+
+       if (message->rcode != dns_rcode_noerror) {
+               dns_zone_log(zone, ISC_LOG_NOTICE,
+                            "checkds: bad DS response from %s: %.*s", addrbuf,
+                            (int)buf.used, rcode);
+               goto failure;
+       }
+
+       /* Lookup DS RRset. */
+       result = dns_message_firstname(message, DNS_SECTION_ANSWER);
+       while (result == ISC_R_SUCCESS) {
+               dns_name_t *name = NULL;
+               dns_rdataset_t *rdataset;
+
+               dns_message_currentname(message, DNS_SECTION_ANSWER, &name);
+               if (dns_name_compare(&zone->origin, name) != 0) {
+                       goto next;
+               }
+
+               for (rdataset = ISC_LIST_HEAD(name->list); rdataset != NULL;
+                    rdataset = ISC_LIST_NEXT(rdataset, link))
+               {
+                       if (rdataset->type != dns_rdatatype_ds) {
+                               goto next;
+                       }
+
+                       ds_rrset = rdataset;
+                       break;
+               }
+
+               if (ds_rrset != NULL) {
+                       break;
+               }
+
+       next:
+               result = dns_message_nextname(message, DNS_SECTION_ANSWER);
+       }
+
+       if (ds_rrset == NULL) {
+               empty = true;
+               dns_zone_log(zone, ISC_LOG_NOTICE,
+                            "checkds: empty DS response from %s", addrbuf);
+       }
+
+       TIME_NOW(&timenow);
+       now = isc_time_seconds(&timenow);
+
+       CHECK(dns_zone_getdb(zone, &db));
+       dns_db_currentversion(db, &version);
+
+       KASP_LOCK(kasp);
+       LOCK_ZONE(zone);
+       for (key = ISC_LIST_HEAD(zone->checkds_ok); key != NULL;
+            key = ISC_LIST_NEXT(key, link))
+       {
+               bool alldone = false, found = false;
+               bool checkdspub = false, checkdsdel = false, ksk = false;
+               dst_key_state_t ds_state = DST_KEY_STATE_NA;
+               isc_stdtime_t published = 0, withdrawn = 0;
+               isc_result_t ret = ISC_R_SUCCESS;
+
+               /* Is this key have the KSK role? */
+               (void)dst_key_role(key->key, &ksk, NULL);
+               if (!ksk) {
+                       continue;
+               }
+
+               /* Do we need to check the DS RRset for this key? */
+               (void)dst_key_getstate(key->key, DST_KEY_DS, &ds_state);
+               (void)dst_key_gettime(key->key, DST_TIME_DSPUBLISH, &published);
+               (void)dst_key_gettime(key->key, DST_TIME_DSDELETE, &withdrawn);
+
+               if (ds_state == DST_KEY_STATE_RUMOURED && published == 0) {
+                       checkdspub = true;
+               } else if (ds_state == DST_KEY_STATE_UNRETENTIVE &&
+                          withdrawn == 0) {
+                       checkdsdel = true;
+               }
+               if (!checkdspub && !checkdsdel) {
+                       continue;
+               }
+
+               if (empty) {
+                       goto dswithdrawn;
+               }
+
+               /* Find the appropriate DS record. */
+               ret = dns_rdataset_first(ds_rrset);
+               while (ret == ISC_R_SUCCESS) {
+                       dns_rdata_ds_t ds;
+                       dns_rdata_t dnskey = DNS_RDATA_INIT;
+                       dns_rdata_t dsrdata = DNS_RDATA_INIT;
+                       dns_rdata_t rdata = DNS_RDATA_INIT;
+                       isc_result_t r;
+                       unsigned char dsbuf[DNS_DS_BUFFERSIZE];
+                       unsigned char keybuf[DST_KEY_MAXSIZE];
+
+                       dns_rdataset_current(ds_rrset, &rdata);
+                       r = dns_rdata_tostruct(&rdata, &ds, NULL);
+                       if (r != ISC_R_SUCCESS) {
+                               goto nextds;
+                       }
+                       /* Check key tag and algorithm. */
+                       if (dst_key_id(key->key) != ds.key_tag) {
+                               goto nextds;
+                       }
+                       if (dst_key_alg(key->key) != ds.algorithm) {
+                               goto nextds;
+                       }
+                       /* Derive DS from DNSKEY, see if the rdata is equal. */
+                       make_dnskey(key->key, keybuf, sizeof(keybuf), &dnskey);
+                       r = dns_ds_buildrdata(&zone->origin, &dnskey,
+                                             ds.digest_type, dsbuf, &dsrdata);
+                       if (r != ISC_R_SUCCESS) {
+                               goto nextds;
+                       }
+                       if (dns_rdata_compare(&rdata, &dsrdata) == 0) {
+                               found = true;
+                               if (checkdspub) {
+                                       /* DS Published. */
+                                       alldone = do_checkds(zone, key->key,
+                                                            now, true);
+                                       if (alldone) {
+                                               rekey = true;
+                                       }
+                               }
+                       }
+
+               nextds:
+                       ret = dns_rdataset_next(ds_rrset);
+               }
+
+       dswithdrawn:
+               /* DS withdrawn. */
+               if (checkdsdel && !found) {
+                       alldone = do_checkds(zone, key->key, now, false);
+                       if (alldone) {
+                               rekey = true;
+                       }
+               }
+       }
+       UNLOCK_ZONE(zone);
+       KASP_UNLOCK(kasp);
+
+       /* Rekey after checkds. */
+       if (rekey) {
+               dns_zone_rekey(zone, false);
+       }
+
+failure:
+       if (result != ISC_R_SUCCESS) {
+               dns_zone_log(zone, ISC_LOG_DEBUG(3),
+                            "checkds: DS request failed: %s",
+                            isc_result_totext(result));
+       }
+
+       if (version != NULL) {
+               dns_db_closeversion(db, &version, false);
+       }
+       if (db != NULL) {
+               dns_db_detach(&db);
+       }
+
+       while (!ISC_LIST_EMPTY(keys)) {
+               key = ISC_LIST_HEAD(keys);
+               ISC_LIST_UNLINK(keys, key, link);
+               dns_dnsseckey_destroy(dns_zone_getmctx(zone), &key);
+       }
+
+       isc_event_free(&event);
+       checkds_destroy(checkds, false);
+       dns_message_detach(&message);
+}
+
+static bool
+checkds_isqueued(dns_zone_t *zone, isc_sockaddr_t *addr, dns_tsigkey_t *key,
+                dns_transport_t *transport) {
+       dns_checkds_t *checkds;
+
+       for (checkds = ISC_LIST_HEAD(zone->checkds_requests); checkds != NULL;
+            checkds = ISC_LIST_NEXT(checkds, link))
+       {
+               if (checkds->request != NULL) {
+                       continue;
+               }
+               if (addr != NULL && isc_sockaddr_equal(addr, &checkds->dst) &&
+                   checkds->key == key && checkds->transport == transport)
+               {
+                       return (true);
+               }
+       }
+       return (false);
+}
+
+static isc_result_t
+checkds_create(isc_mem_t *mctx, unsigned int flags, dns_checkds_t **checkdsp) {
+       dns_checkds_t *checkds;
+
+       REQUIRE(checkdsp != NULL && *checkdsp == NULL);
+
+       checkds = isc_mem_get(mctx, sizeof(*checkds));
+       *checkds = (dns_checkds_t){
+               .flags = flags,
+       };
+
+       isc_mem_attach(mctx, &checkds->mctx);
+       isc_sockaddr_any(&checkds->dst);
+       ISC_LINK_INIT(checkds, link);
+       checkds->magic = CHECKDS_MAGIC;
+       *checkdsp = checkds;
+       return (ISC_R_SUCCESS);
+}
+
+static isc_result_t
+checkds_createmessage(dns_zone_t *zone, dns_message_t **messagep) {
+       dns_message_t *message = NULL;
+
+       dns_name_t *tempname = NULL;
+       dns_rdataset_t *temprdataset = NULL;
+
+       isc_result_t result;
+
+       REQUIRE(DNS_ZONE_VALID(zone));
+       REQUIRE(messagep != NULL && *messagep == NULL);
+
+       dns_message_create(zone->mctx, DNS_MESSAGE_INTENTRENDER, &message);
+
+       message->opcode = dns_opcode_query;
+       message->rdclass = zone->rdclass;
+
+       result = dns_message_gettempname(message, &tempname);
+       if (result != ISC_R_SUCCESS) {
+               goto cleanup;
+       }
+
+       result = dns_message_gettemprdataset(message, &temprdataset);
+       if (result != ISC_R_SUCCESS) {
+               goto cleanup;
+       }
+
+       /*
+        * Make question.
+        */
+       dns_name_init(tempname, NULL);
+       dns_name_clone(&zone->origin, tempname);
+       dns_rdataset_makequestion(temprdataset, zone->rdclass,
+                                 dns_rdatatype_ds);
+       ISC_LIST_APPEND(tempname->list, temprdataset, link);
+       dns_message_addname(message, tempname, DNS_SECTION_QUESTION);
+       tempname = NULL;
+       temprdataset = NULL;
+
+       *messagep = message;
+       return (ISC_R_SUCCESS);
+
+cleanup:
+       if (tempname != NULL) {
+               dns_message_puttempname(message, &tempname);
+       }
+       if (temprdataset != NULL) {
+               dns_message_puttemprdataset(message, &temprdataset);
+       }
+       dns_message_detach(&message);
+       return (result);
+}
+
+static void
+checkds_send_toaddr(isc_task_t *task, isc_event_t *event) {
+       dns_checkds_t *checkds;
+       isc_result_t result;
+       dns_message_t *message = NULL;
+       isc_netaddr_t dstip;
+       dns_tsigkey_t *key = NULL;
+       char addrbuf[ISC_SOCKADDR_FORMATSIZE];
+       isc_sockaddr_t src;
+       unsigned int options, timeout;
+       bool have_checkdssource = false;
+       bool have_checkdsdscp = false;
+       isc_dscp_t dscp = -1;
+
+       checkds = event->ev_arg;
+       REQUIRE(DNS_CHECKDS_VALID(checkds));
+
+       UNUSED(task);
+
+       LOCK_ZONE(checkds->zone);
+
+       checkds->event = NULL;
+
+       if (DNS_ZONE_FLAG(checkds->zone, DNS_ZONEFLG_LOADED) == 0) {
+               result = ISC_R_CANCELED;
+               goto cleanup;
+       }
+
+       if ((event->ev_attributes & ISC_EVENTATTR_CANCELED) != 0 ||
+           DNS_ZONE_FLAG(checkds->zone, DNS_ZONEFLG_EXITING) ||
+           checkds->zone->view->requestmgr == NULL ||
+           checkds->zone->db == NULL)
+       {
+               result = ISC_R_CANCELED;
+               goto cleanup;
+       }
+
+       /*
+        * The raw IPv4 address should also exist.  Don't send to the
+        * mapped form.
+        */
+       if (isc_sockaddr_pf(&checkds->dst) == PF_INET6 &&
+           IN6_IS_ADDR_V4MAPPED(&checkds->dst.type.sin6.sin6_addr))
+       {
+               isc_sockaddr_format(&checkds->dst, addrbuf, sizeof(addrbuf));
+               dns_zone_log(checkds->zone, ISC_LOG_DEBUG(3),
+                            "checkds: ignoring IPv6 mapped IPV4 address: %s",
+                            addrbuf);
+               result = ISC_R_CANCELED;
+               goto cleanup;
+       }
+
+       result = checkds_createmessage(checkds->zone, &message);
+       if (result != ISC_R_SUCCESS) {
+               goto cleanup;
+       }
+
+       isc_sockaddr_format(&checkds->dst, addrbuf, sizeof(addrbuf));
+       if (checkds->key != NULL) {
+               /* Transfer ownership of key */
+               key = checkds->key;
+               checkds->key = NULL;
+       } else {
+               isc_netaddr_fromsockaddr(&dstip, &checkds->dst);
+               result = dns_view_getpeertsig(checkds->zone->view, &dstip,
+                                             &key);
+               if (result != ISC_R_SUCCESS && result != ISC_R_NOTFOUND) {
+                       dns_zone_log(checkds->zone, ISC_LOG_ERROR,
+                                    "checkds: DS query to %s not sent. "
+                                    "Peer TSIG key lookup failure.",
+                                    addrbuf);
+                       goto cleanup_message;
+               }
+       }
+
+       if (key != NULL) {
+               char namebuf[DNS_NAME_FORMATSIZE];
+
+               dns_name_format(&key->name, namebuf, sizeof(namebuf));
+               dns_zone_log(checkds->zone, ISC_LOG_DEBUG(3),
+                            "checkds: sending DS query to %s : TSIG (%s)",
+                            addrbuf, namebuf);
+       } else {
+               dns_zone_log(checkds->zone, ISC_LOG_DEBUG(3),
+                            "checkds: sending DS query to %s", addrbuf);
+       }
+       options = 0;
+       if (checkds->zone->view->peers != NULL) {
+               dns_peer_t *peer = NULL;
+               bool usetcp = false;
+               result = dns_peerlist_peerbyaddr(checkds->zone->view->peers,
+                                                &dstip, &peer);
+               if (result == ISC_R_SUCCESS) {
+                       result = dns_peer_getquerysource(peer, &src);
+                       if (result == ISC_R_SUCCESS) {
+                               have_checkdssource = true;
+                       }
+                       dns_peer_getquerydscp(peer, &dscp);
+                       if (dscp != -1) {
+                               have_checkdsdscp = true;
+                       }
+                       result = dns_peer_getforcetcp(peer, &usetcp);
+                       if (result == ISC_R_SUCCESS && usetcp) {
+                               options |= DNS_FETCHOPT_TCP;
+                       }
+               }
+       }
+       switch (isc_sockaddr_pf(&checkds->dst)) {
+       case PF_INET:
+               if (!have_checkdssource) {
+                       src = checkds->zone->parentalsrc4;
+               }
+               if (!have_checkdsdscp) {
+                       dscp = checkds->zone->parentalsrc4dscp;
+               }
+               break;
+       case PF_INET6:
+               if (!have_checkdssource) {
+                       src = checkds->zone->parentalsrc6;
+               }
+               if (!have_checkdsdscp) {
+                       dscp = checkds->zone->parentalsrc6dscp;
+               }
+               break;
+       default:
+               result = ISC_R_NOTIMPLEMENTED;
+               goto cleanup_key;
+       }
+
+       dns_zone_log(checkds->zone, ISC_LOG_DEBUG(3),
+                    "checkds: create request for DS query to %s", addrbuf);
+
+       timeout = 15;
+       options |= DNS_REQUESTOPT_TCP;
+       result = dns_request_createvia(
+               checkds->zone->view->requestmgr, message, &src, &checkds->dst,
+               dscp, options, key, timeout * 3, timeout, 0,
+               checkds->zone->task, checkds_done, checkds, &checkds->request);
+       if (result != ISC_R_SUCCESS) {
+               dns_zone_log(
+                       checkds->zone, ISC_LOG_DEBUG(3),
+                       "checkds: dns_request_createvia() to %s failed: %s",
+                       addrbuf, dns_result_totext(result));
+               goto cleanup;
+       }
+
+cleanup_key:
+       if (key != NULL) {
+               dns_tsigkey_detach(&key);
+       }
+cleanup_message:
+       dns_message_detach(&message);
+cleanup:
+       UNLOCK_ZONE(checkds->zone);
+       isc_event_free(&event);
+       if (result != ISC_R_SUCCESS) {
+               checkds_destroy(checkds, false);
+       }
+}
+
+static isc_result_t
+checkds_send_queue(dns_checkds_t *checkds) {
+       isc_event_t *e;
+       isc_result_t result;
+
+       INSIST(checkds->event == NULL);
+       e = isc_event_allocate(checkds->mctx, NULL, DNS_EVENT_CHECKDSSENDTOADDR,
+                              checkds_send_toaddr, checkds,
+                              sizeof(isc_event_t));
+       e->ev_arg = checkds;
+       e->ev_sender = NULL;
+       result = isc_ratelimiter_enqueue(checkds->zone->zmgr->checkdsrl,
+                                        checkds->zone->task, &e);
+       if (result != ISC_R_SUCCESS) {
+               isc_event_free(&e);
+               checkds->event = NULL;
+       }
+       return (result);
+}
+
+static void
+checkds_send(dns_zone_t *zone) {
+       dns_view_t *view = dns_zone_getview(zone);
+       isc_result_t result;
+       unsigned int flags = 0;
+
+       /*
+        * Zone lock held by caller.
+        */
+       REQUIRE(LOCKED_ZONE(zone));
+
+       dns_zone_log(zone, ISC_LOG_DEBUG(3),
+                    "checkds: start sending DS queries to %u parentals",
+                    zone->parentalscnt);
+
+       if (DNS_ZONE_FLAG(zone, DNS_ZONEFLG_EXITING)) {
+               dns_zone_log(zone, ISC_LOG_DEBUG(3),
+                            "checkds: abort, named exiting");
+               return;
+       }
+
+       for (unsigned int i = 0; i < zone->parentalscnt; i++) {
+               dns_tsigkey_t *key = NULL;
+               dns_transport_t *transport = NULL;
+               isc_sockaddr_t dst;
+               dns_checkds_t *checkds = NULL;
+
+               if ((zone->parentalkeynames != NULL) &&
+                   (zone->parentalkeynames[i] != NULL)) {
+                       dns_name_t *keyname = zone->parentalkeynames[i];
+                       (void)dns_view_gettsig(view, keyname, &key);
+               }
+
+               if ((zone->parentaltlsnames != NULL) &&
+                   (zone->parentaltlsnames[i] != NULL)) {
+                       dns_name_t *tlsname = zone->parentaltlsnames[i];
+                       (void)dns_view_gettransport(view, DNS_TRANSPORT_TLS,
+                                                   tlsname, &transport);
+                       dns_zone_logc(
+                               zone, DNS_LOGCATEGORY_XFER_IN, ISC_LOG_ERROR,
+                               "got TLS configuration for zone transfer");
+               }
+
+               dst = zone->parentals[i];
+
+               /* TODO: glue the transport to the checkds request */
+
+               if (checkds_isqueued(zone, &dst, key, transport)) {
+                       dns_zone_log(zone, ISC_LOG_DEBUG(3),
+                                    "checkds: DS query to parent "
+                                    "%d is queued",
+                                    i);
+                       if (key != NULL) {
+                               dns_tsigkey_detach(&key);
+                       }
+                       if (transport != NULL) {
+                               dns_transport_detach(&transport);
+                       }
+                       continue;
+               }
+
+               dns_zone_log(zone, ISC_LOG_DEBUG(3),
+                            "checkds: create DS query for "
+                            "parent %d",
+                            i);
+
+               result = checkds_create(zone->mctx, flags, &checkds);
+               if (result != ISC_R_SUCCESS) {
+                       dns_zone_log(zone, ISC_LOG_DEBUG(3),
+                                    "checkds: create DS query for "
+                                    "parent %d failed",
+                                    i);
+                       continue;
+               }
+               zone_iattach(zone, &checkds->zone);
+               checkds->dst = dst;
+
+               INSIST(checkds->key == NULL);
+               if (key != NULL) {
+                       checkds->key = key;
+                       key = NULL;
+               }
+
+               INSIST(checkds->transport == NULL);
+               if (transport != NULL) {
+                       checkds->transport = transport;
+                       transport = NULL;
+               }
+
+               ISC_LIST_APPEND(zone->checkds_requests, checkds, link);
+               result = checkds_send_queue(checkds);
+               if (result != ISC_R_SUCCESS) {
+                       dns_zone_log(zone, ISC_LOG_DEBUG(3),
+                                    "checkds: send DS query to "
+                                    "parent %d failed",
+                                    i);
+                       checkds_destroy(checkds, true);
+               }
+       }
+}
+
+static void
+zone_checkds(dns_zone_t *zone) {
+       bool cdscheck = false;
+
+       for (dns_dnsseckey_t *key = ISC_LIST_HEAD(zone->checkds_ok);
+            key != NULL; key = ISC_LIST_NEXT(key, link))
+       {
+               dst_key_state_t ds_state = DST_KEY_STATE_NA;
+               bool ksk = false;
+               isc_stdtime_t published = 0, withdrawn = 0;
+
+               /* Is this key have the KSK role? */
+               (void)dst_key_role(key->key, &ksk, NULL);
+               if (!ksk) {
+                       continue;
+               }
+
+               /* Do we need to check the DS RRset? */
+               (void)dst_key_getstate(key->key, DST_KEY_DS, &ds_state);
+               (void)dst_key_gettime(key->key, DST_TIME_DSPUBLISH, &published);
+               (void)dst_key_gettime(key->key, DST_TIME_DSDELETE, &withdrawn);
+
+               if (ds_state == DST_KEY_STATE_RUMOURED && published == 0) {
+                       dst_key_setnum(key->key, DST_NUM_DSPUBCOUNT, 0);
+                       cdscheck = true;
+               } else if (ds_state == DST_KEY_STATE_UNRETENTIVE &&
+                          withdrawn == 0) {
+                       dst_key_setnum(key->key, DST_NUM_DSDELCOUNT, 0);
+                       cdscheck = true;
+               }
+       }
+
+       if (cdscheck) {
+               /* Request the DS RRset. */
+               LOCK_ZONE(zone);
+               checkds_send(zone);
+               UNLOCK_ZONE(zone);
+       }
+}
+
 static void
 zone_rekey(dns_zone_t *zone) {
        isc_result_t result;
@@ -20383,10 +21211,12 @@ zone_rekey(dns_zone_t *zone) {
        fullsign = DNS_ZONEKEY_OPTION(zone, DNS_ZONEKEY_FULLSIGN);
 
        KASP_LOCK(kasp);
-       dns_zone_lock_keyfiles(zone);
 
+       dns_zone_lock_keyfiles(zone);
        result = dns_dnssec_findmatchingkeys(&zone->origin, dir, now, mctx,
                                             &keys);
+       dns_zone_unlock_keyfiles(zone);
+
        if (result != ISC_R_SUCCESS) {
                dnssec_log(zone, ISC_LOG_DEBUG(1),
                           "zone_rekey:dns_dnssec_findmatchingkeys failed: %s",
@@ -20394,23 +21224,45 @@ zone_rekey(dns_zone_t *zone) {
        }
 
        if (kasp != NULL) {
+               /*
+                * Check DS at parental agents. Clear ongoing checks.
+                */
+               LOCK_ZONE(zone);
+               checkds_cancel(zone);
+               clear_keylist(&zone->checkds_ok, zone->mctx);
+               ISC_LIST_INIT(zone->checkds_ok);
+               UNLOCK_ZONE(zone);
+
+               result = dns_zone_getdnsseckeys(zone, db, ver, now,
+                                               &zone->checkds_ok);
+
+               if (result != ISC_R_SUCCESS) {
+                       dnssec_log(zone, ISC_LOG_ERROR,
+                                  "zone_rekey:dns_zone_getdnsseckeys failed: "
+                                  "%s",
+                                  isc_result_totext(result));
+               } else {
+                       zone_checkds(zone);
+               }
+
                if (result == ISC_R_SUCCESS || result == ISC_R_NOTFOUND) {
+                       dns_zone_lock_keyfiles(zone);
                        result = dns_keymgr_run(&zone->origin, zone->rdclass,
                                                dir, mctx, &keys, &dnskeys,
                                                kasp, now, &nexttime);
+                       dns_zone_unlock_keyfiles(zone);
+
                        if (result != ISC_R_SUCCESS) {
                                dnssec_log(zone, ISC_LOG_ERROR,
                                           "zone_rekey:dns_dnssec_keymgr "
                                           "failed: %s",
                                           isc_result_totext(result));
-                               dns_zone_unlock_keyfiles(zone);
                                KASP_UNLOCK(kasp);
                                goto failure;
                        }
                }
        }
 
-       dns_zone_unlock_keyfiles(zone);
        KASP_UNLOCK(kasp);
 
        if (result == ISC_R_SUCCESS) {