Commit 525679f60f for bind

commit 525679f60f4081e9386378723639cc9d82b84893
Author: Evan Hunt <each@isc.org>
Date:   Wed Oct 21 17:55:15 2026 -0700

    Add a warning when signing DELEG without an ADT key

    When signing an RRset of type DELEG, named and dnssec-signzone
    will check that at least one key in the DNSKEY RRset has the
    ADT flag set. If not, a warning will be logged.

    (In the future, this might become a fatal error, with an
    optional switch to disable it.)

diff --git a/bin/dnssec/dnssec-signzone.c b/bin/dnssec/dnssec-signzone.c
index 98eb28daf7..687db5df24 100644
--- a/bin/dnssec/dnssec-signzone.c
+++ b/bin/dnssec/dnssec-signzone.c
@@ -182,6 +182,7 @@ static bool set_maxttl = false;
 static dns_ttl_t maxttl = 0;
 static bool no_max_check = false;
 static const char *sync_records = "cdnskey,cds:sha-256";
+static bool have_adt = false;

 #define INCSTAT(counter)                               \
 	if (printstats) {                              \
@@ -540,6 +541,13 @@ signset(dns_diff_t *del, dns_diff_t *add, dns_dbnode_t *node, dns_name_t *name,

 	vbprintf(1, "%s/%s:\n", namestr, typestr);

+	if (set->type == dns_rdatatype_deleg && !have_adt) {
+		fprintf(stderr,
+			"%s: warning: signing DELEG, but no DNSKEY with "
+			"the ADT flag is present.\n",
+			isc_commandline_progname);
+	}
+
 	RWLOCK(&keylist_lock, isc_rwlocktype_read);
 	arraysize = keycount;
 	RWUNLOCK(&keylist_lock, isc_rwlocktype_read);
@@ -2877,7 +2885,7 @@ findkeys:
 }

 static void
-warnifallksk(dns_db_t *db) {
+checkdnskeys(dns_db_t *db) {
 	dns_dbversion_t *currentversion = NULL;
 	dns_dbnode_t *node = NULL;
 	dns_rdataset_t rdataset;
@@ -2913,6 +2921,11 @@ warnifallksk(dns_db_t *db) {
 		check_result(result, "dns_rdata_tostruct");
 		if ((dnskey.flags & DNS_KEYFLAG_KSK) == 0) {
 			have_non_ksk = true;
+		}
+		if ((dnskey.flags & DNS_KEYFLAG_ADT) != 0) {
+			have_adt = true;
+		}
+		if (have_non_ksk && have_adt) {
 			break;
 		}
 	}
@@ -3871,7 +3884,7 @@ main(int argc, char *argv[]) {
 		nokeys = true;
 	}

-	warnifallksk(gdb);
+	checkdnskeys(gdb);

 	if (IS_NSEC3) {
 		bool answer;
diff --git a/bin/tests/system/deleg/ns1/named.conf.j2 b/bin/tests/system/deleg/ns1/named.conf.j2
index 444c03b3a5..e6927cbbcf 100644
--- a/bin/tests/system/deleg/ns1/named.conf.j2
+++ b/bin/tests/system/deleg/ns1/named.conf.j2
@@ -31,7 +31,7 @@ dnssec-policy nsec {
 	parent-ds-ttl 3600;
 	keys {
 		zsk key-directory lifetime 60d algorithm ecdsa256;
-		ksk key-directory lifetime unlimited algorithm ecdsa256;
+		ksk key-directory lifetime unlimited algorithm ecdsa256 flags sep adt;
 	};
 };

@@ -41,7 +41,7 @@ dnssec-policy nsec3 {
 	nsec3param iterations 0 optout no;
 	keys {
 		zsk key-directory lifetime 60d algorithm ecdsa256;
-		ksk key-directory lifetime unlimited algorithm ecdsa256;
+		ksk key-directory lifetime unlimited algorithm ecdsa256 flags sep adt;
 	};
 };

@@ -51,7 +51,7 @@ dnssec-policy optout {
 	nsec3param iterations 0 optout yes;
 	keys {
 		zsk key-directory lifetime 60d algorithm ecdsa256;
-		ksk key-directory lifetime unlimited algorithm ecdsa256;
+		ksk key-directory lifetime unlimited algorithm ecdsa256 flags sep adt;
 	};
 };

@@ -83,3 +83,10 @@ zone unsigned {
 	initial-file "example.db.in";
 	dnssec-policy none;
 };
+
+zone noadt {
+	type primary;
+	file "noadt.db";
+	initial-file "example.db.in";
+	dnssec-policy default;
+};
diff --git a/bin/tests/system/deleg/tests_auth.py b/bin/tests/system/deleg/tests_auth.py
index b84f45311d..76d6c0b780 100644
--- a/bin/tests/system/deleg/tests_auth.py
+++ b/bin/tests/system/deleg/tests_auth.py
@@ -9,6 +9,8 @@
 # See the COPYRIGHT file distributed with this work for additional
 # information regarding copyright ownership.

+from re import compile as Re
+
 import time

 from dns import rdatatype, update
@@ -439,3 +441,11 @@ def test_deleg_updates(qtype, do, de):
         if de:
             expected = 4  # signed DELEG, signed NSEC/NSEC3
     isctest.check.rr_count_eq(res.authority, expected)
+
+
+def test_deleg_noadt_warning(ns1):
+    # this should be found for zone noadt and no other zones
+    count1 = len(ns1.log.grep(Re("noadt/IN.*signing DELEG but no DNSKEY")))
+    count2 = len(ns1.log.grep(Re("signing DELEG but no DNSKEY")))
+    assert count1 != 0
+    assert count1 == count2
diff --git a/lib/dns/update.c b/lib/dns/update.c
index 9e17819ae1..8c2e71e2cf 100644
--- a/lib/dns/update.c
+++ b/lib/dns/update.c
@@ -920,8 +920,8 @@ find_zone_keys(dns_zone_t *zone, isc_mem_t *mctx, unsigned int maxkeys,
 static isc_result_t
 add_sigs(dns_update_log_t *log, dns_zone_t *zone, dns_db_t *db,
 	 dns_dbversion_t *ver, dns_name_t *name, dns_rdatatype_t type,
-	 dns_diff_t *diff, dst_key_t **keys, unsigned int nkeys,
-	 isc_stdtime_t now, isc_stdtime_t inception, isc_stdtime_t expire) {
+	 dns_diff_t *diff, dst_key_t **keys, size_t nkeys, isc_stdtime_t now,
+	 isc_stdtime_t inception, isc_stdtime_t expire) {
 	isc_result_t result;
 	dns_dbnode_t *node = NULL;
 	dns_kasp_t *kasp = dns_zone_getkasp(zone);
@@ -930,7 +930,6 @@ add_sigs(dns_update_log_t *log, dns_zone_t *zone, dns_db_t *db,
 	dns_stats_t *dnssecsignstats = dns_zone_getdnssecsignstats(zone);
 	isc_buffer_t buffer;
 	unsigned char data[DNS_RDATA_MAXLENGTH];
-	unsigned int i;
 	bool added_sig = false;
 	bool use_kasp = false;
 	bool offlineksk = false;
@@ -956,15 +955,32 @@ add_sigs(dns_update_log_t *log, dns_zone_t *zone, dns_db_t *db,

 #define REVOKE(x) ((dst_key_flags(x) & DNS_KEYFLAG_REVOKE) != 0)
 #define KSK(x)	  ((dst_key_flags(x) & DNS_KEYFLAG_KSK) != 0)
+#define ADT(x)	  ((dst_key_flags(x) & DNS_KEYFLAG_ADT) != 0)
 #define ID(x)	  dst_key_id(x)
 #define ALG(x)	  dst_key_alg(x)

+	if (type == dns_rdatatype_deleg) {
+		/* Warn if we don't have at least one ADT key */
+		bool adt = false;
+		for (size_t i = 0; i < nkeys; i++) {
+			if (ADT(keys[i])) {
+				adt = true;
+				break;
+			}
+		}
+		if (!adt) {
+			update_log(log, zone, ISC_LOG_WARNING,
+				   "WARNING: signing DELEG but no DNSKEY "
+				   "with the ADT flag is present");
+		}
+	}
+
 	/*
 	 * If we are honoring KSK flags then we need to check that we
 	 * have both KSK and non-KSK keys that are not revoked per
 	 * algorithm.
 	 */
-	for (i = 0; i < nkeys; i++) {
+	for (size_t i = 0; i < nkeys; i++) {
 		bool both = false;

 		/* Don't add signatures for offline or inactive keys */
diff --git a/lib/dns/zone.c b/lib/dns/zone.c
index f24df3642f..6569493951 100644
--- a/lib/dns/zone.c
+++ b/lib/dns/zone.c
@@ -116,6 +116,7 @@
  */
 #define REVOKE(x)  ((dst_key_flags(x) & DNS_KEYFLAG_REVOKE) != 0)
 #define KSK(x)	   ((dst_key_flags(x) & DNS_KEYFLAG_KSK) != 0)
+#define ADT(x)	   ((dst_key_flags(x) & DNS_KEYFLAG_ADT) != 0)
 #define ZONEKEY(x) ((dst_key_flags(x) & DNS_KEYOWNER_ZONE) != 0)
 #define ID(x)	   dst_key_id(x)
 #define ALG(x)	   dst_key_alg(x)
@@ -5764,9 +5765,9 @@ cleanup:

 static isc_result_t
 add_sigs(dns_db_t *db, dns_dbversion_t *ver, dns_name_t *name, dns_zone_t *zone,
-	 dns_rdatatype_t type, dns_diff_t *diff, dst_key_t **keys,
-	 unsigned int nkeys, isc_mem_t *mctx, isc_stdtime_t now,
-	 isc_stdtime_t inception, isc_stdtime_t expire) {
+	 dns_rdatatype_t type, dns_diff_t *diff, dst_key_t **keys, size_t nkeys,
+	 isc_mem_t *mctx, isc_stdtime_t now, isc_stdtime_t inception,
+	 isc_stdtime_t expire) {
 	isc_result_t result;
 	dns_dbnode_t *node = NULL;
 	dns_stats_t *dnssecsignstats;
@@ -5774,7 +5775,6 @@ add_sigs(dns_db_t *db, dns_dbversion_t *ver, dns_name_t *name, dns_zone_t *zone,
 	dns_rdata_t sig_rdata = DNS_RDATA_INIT;
 	unsigned char data[DNS_RDATA_MAXLENGTH];
 	isc_buffer_t buffer;
-	unsigned int i;
 	bool use_kasp = false;
 	bool offlineksk = false;

@@ -5808,7 +5808,23 @@ add_sigs(dns_db_t *db, dns_dbversion_t *ver, dns_name_t *name, dns_zone_t *zone,
 		goto cleanup;
 	}

-	for (i = 0; i < nkeys; i++) {
+	if (type == dns_rdatatype_deleg) {
+		/* Warn if we don't have at least one ADT key */
+		bool adt = false;
+		for (size_t i = 0; i < nkeys; i++) {
+			if (ADT(keys[i])) {
+				adt = true;
+				break;
+			}
+		}
+		if (!adt) {
+			dns_zone_log(zone, ISC_LOG_WARNING,
+				     "WARNING: signing DELEG but no DNSKEY "
+				     "with the ADT flag is present");
+		}
+	}
+
+	for (size_t i = 0; i < nkeys; i++) {
 		/* Don't add signatures for offline or inactive keys */
 		if (!dst_key_isprivate(keys[i]) && !offlineksk) {
 			continue;
@@ -6462,8 +6478,8 @@ sign_a_node(dns_db_t *db, dns_zone_t *zone, dns_name_t *name,
 	    bool build_nsec, dst_key_t *key, isc_stdtime_t now,
 	    isc_stdtime_t inception, isc_stdtime_t expire, dns_ttl_t nsecttl,
 	    bool both, bool is_ksk, bool is_zsk, bool fullsign,
-	    bool is_bottom_of_zone, dns_diff_t *diff, int32_t *signatures,
-	    isc_mem_t *mctx) {
+	    bool is_bottom_of_zone, bool have_adt, dns_diff_t *diff,
+	    int32_t *signatures, isc_mem_t *mctx) {
 	isc_result_t result;
 	dns_rdatasetiter_t *iterator = NULL;
 	dns_rdataset_t rdataset = DNS_RDATASET_INIT;
@@ -6558,6 +6574,12 @@ sign_a_node(dns_db_t *db, dns_zone_t *zone, dns_name_t *name,
 			continue;
 		}

+		if (rdataset.type == dns_rdatatype_deleg && !have_adt) {
+			dns_zone_log(zone, ISC_LOG_WARNING,
+				     "WARNING: signing DELEG but no DNSKEY "
+				     "with the ADT flag is present");
+		}
+
 		/* Calculate the signature, creating a RRSIG RDATA. */
 		isc_buffer_clear(&buffer);
 		if (offlineksk && dns_rdatatype_iskeymaterial(rdataset.type)) {
@@ -8321,6 +8343,7 @@ zone_sign(dns_zone_t *zone) {
 	while (signing != NULL && nodes-- > 0 && signatures > 0) {
 		dns_signing_t *nextsigning = ISC_LIST_NEXT(signing, link);
 		bool has_alg = false;
+		bool have_adt = false;

 		dns_dbiterator_pause(signing->dbiterator);

@@ -8401,6 +8424,14 @@ zone_sign(dns_zone_t *zone) {
 			}
 		}

+		/* Check whether we still have any ADT keys */
+		for (i = 0; i < nkeys; i++) {
+			if (ADT(zone_keys[i])) {
+				have_adt = true;
+				break;
+			}
+		}
+
 		/*
 		 * Process one node.
 		 */
@@ -8509,7 +8540,8 @@ zone_sign(dns_zone_t *zone) {
 				build_nsec, zone_keys[i], now, inception,
 				expire, zone_nsecttl(zone), both, is_ksk,
 				is_zsk, signing->fullsign, is_bottom_of_zone,
-				zonediff.diff, &signatures, zone->mctx));
+				have_adt, zonediff.diff, &signatures,
+				zone->mctx));
 			/*
 			 * If we are adding we are done.  Look for other keys
 			 * of the same algorithm if deleting.