Commit de3079c20d for bind

commit de3079c20d2173eeffca8d0a8c2468313173be5a
Author: Nicki Křížek <nicki@isc.org>
Date:   Fri Oct 2 14:02:24 2026 +0000

    Ignore key files of other keys with the same key tag

    named finds the files of a key by its key tag. Key files are named after
    the zone, the algorithm, and the key tag (K<zone>+<alg>+<tag>), and for
    each DNSKEY in the zone, named built that file name and opened the file,
    without checking that the key in it is the key in the DNSKEY.

    With offline-ksk, the KSK and the ZSKs are generated separately, and
    named only has the files of the ZSKs. The signatures over the DNSKEY,
    CDS, and CDNSKEY RRsets are made in advance by whoever holds the KSK,
    and reach named in the imported SKR. Nothing prevents a ZSK from getting
    the same key tag as the KSK, and when that happened, named opened the
    ZSK's files for the KSK's DNSKEY. It then believed it had the ZSK twice
    and no KSK:

    - Only keys in the KSK role sign the DNSKEY RRset, so named never took
      the KSK's signature from the SKR, and left the DNSKEY RRset unsigned.
      Validating resolvers then cannot trust the zone and treat it as bogus.

    - named signed some RRsets, such as the SOA, twice with the ZSK. With
      deterministic signatures (Ed25519 and Ed448, and ECDSA with OpenSSL
      3.2 and later outside of FIPS mode), the two signatures came out
      identical, adding the second one failed with "not exact", and so did
      the whole key update, which named retried every ten minutes.

    The same mix-up happens whenever the DNSKEY RRset holds a key whose
    files named does not have, but which shares the key tag of a key whose
    files named does have, e.g. the DNSKEY of another provider in a
    multi-signer setup.

    Check that a key file found by its key tag holds the same key as the
    DNSKEY, and treat the file as missing otherwise, just as when there is
    no file at all, e.g. for an offline KSK or another provider's key. When
    the policy uses several key-stores, look for the matching file in the
    remaining ones.

    Assisted-by: Claude:claude-opus-5-5

diff --git a/lib/dns/zone.c b/lib/dns/zone.c
index 5685e329ac..d1930727f8 100644
--- a/lib/dns/zone.c
+++ b/lib/dns/zone.c
@@ -5005,6 +5005,29 @@ was_dumping(dns_zone_t *zone) {
 	return false;
 }

+/*
+ * Key files are named after the key tag, but distinct keys may share a key
+ * tag, e.g. an offline KSK and a ZSK.  Treat a key file holding a different
+ * key than 'pubkey' as missing.
+ */
+static isc_result_t
+keyfromdir(dst_key_t *pubkey, const char *directory, isc_mem_t *mctx,
+	   dst_key_t **key) {
+	isc_result_t result;
+
+	result = dst_key_fromfile(
+		dst_key_name(pubkey), dst_key_id(pubkey), dst_key_alg(pubkey),
+		DST_TYPE_PUBLIC | DST_TYPE_PRIVATE | DST_TYPE_STATE, directory,
+		mctx, key);
+	if (result == ISC_R_SUCCESS && !dst_key_pubcompare(pubkey, *key, true))
+	{
+		dst_key_free(key);
+		result = ISC_R_FILENOTFOUND;
+	}
+
+	return result;
+}
+
 static isc_result_t
 keyfromfile(dns_zone_t *zone, dst_key_t *pubkey, isc_mem_t *mctx,
 	    dst_key_t **key) {
@@ -5016,23 +5039,14 @@ keyfromfile(dns_zone_t *zone, dst_key_t *pubkey, isc_mem_t *mctx,
 	if (kasp == NULL || (strcmp(dns_kasp_getname(kasp), "none") == 0) ||
 	    (strcmp(dns_kasp_getname(kasp), "insecure") == 0))
 	{
-		result = dst_key_fromfile(
-			dst_key_name(pubkey), dst_key_id(pubkey),
-			dst_key_alg(pubkey),
-			DST_TYPE_PUBLIC | DST_TYPE_PRIVATE | DST_TYPE_STATE,
-			directory, mctx, &foundkey);
+		result = keyfromdir(pubkey, directory, mctx, &foundkey);
 	} else {
 		ISC_LIST_FOREACH(dns_kasp_keys(kasp), kkey, link) {
 			dns_keystore_t *ks = dns_kasp_key_keystore(kkey);
 			directory = dns_keystore_directory(ks,
 							   zone->keydirectory);

-			result = dst_key_fromfile(
-				dst_key_name(pubkey), dst_key_id(pubkey),
-				dst_key_alg(pubkey),
-				DST_TYPE_PUBLIC | DST_TYPE_PRIVATE |
-					DST_TYPE_STATE,
-				directory, mctx, &foundkey);
+			result = keyfromdir(pubkey, directory, mctx, &foundkey);
 			if (result == ISC_R_SUCCESS) {
 				break;
 			}