[9.20] fix: usr: CDS/CDNSKEY records were not removed when re-configuring the server

When on an ``rndc reconfig`` the DNSSEC policy changes such that it changes the expected ``CDNSKEY`` and/or ``CDS`` records in the zone, the RRset should
be updated accordingly. This did not happen when removing digests from the configuration, or setting `cdnskey no;`. This has been fixed.

Closes #6166

Backport of MR !12265

Merge branch 'backport-6166-reconfig-delete-cds-9.20' into 'bind-9.20'

See merge request isc-projects/bind9!12284
This commit is contained in:
Matthijs Mekking 2026-06-19 11:40:37 +00:00
commit 20db02121f
11 changed files with 231 additions and 46 deletions

View file

@ -87,8 +87,9 @@ sed 's/ add \(.*\) IN DS / add \1 3600 IN DS /' <UP.swap >UP.swapttl
sign() {
cat >db.$1
cat $keyz.key $key1.key $key2.key >>db.$1
$SIGNER >/dev/null \
-S -O full -o $Z -f sig.$1 db.$1
-O full -o $Z -f sig.$1 db.$1 $keyz $key1 $key2
}
sign null <<EOF

View file

@ -0,0 +1,6 @@
/* A zone which has CDS and CDNSKEY published. */
zone "cds-cdnskey.kasp" {
type primary;
file "cds-cdnskey.kasp.db";
dnssec-policy "cds-cdnskey";
};

View file

@ -0,0 +1,6 @@
/* A zone which has CDS and CDNSKEY published. */
zone "cds-cdnskey.kasp" {
type primary;
file "cds-cdnskey.kasp.db";
dnssec-policy "cds-cdnskey-no";
};

View file

@ -0,0 +1,6 @@
/* A zone which has CDS and CDNSKEY published. */
zone "cds-cdnskey.kasp" {
type primary;
file "cds-cdnskey.kasp.db";
dnssec-policy "cds-cdnskey-alt";
};

View file

@ -15,6 +15,7 @@ include "policies/autosign.conf";
include "policies/kasp-fips.conf";
include "named-common.conf";
include "named-fips.conf";
include "named-reconfig.conf";
{% if RSASHA1_SUPPORTED == "1" %}
include "policies/kasp-rsasha1.conf";

View file

@ -119,3 +119,36 @@ dnssec-policy "keystore" {
zsk key-store "zsk" lifetime unlimited algorithm @DEFAULT_ALGORITHM@;
};
};
dnssec-policy "cds-cdnskey" {
dnskey-ttl 3600;
keys {
ksk key-directory lifetime unlimited algorithm @DEFAULT_ALGORITHM@;
zsk key-directory lifetime unlimited algorithm @DEFAULT_ALGORITHM@;
};
};
dnssec-policy "cds-cdnskey-no" {
dnskey-ttl 3600;
keys {
ksk key-directory lifetime unlimited algorithm @DEFAULT_ALGORITHM@;
zsk key-directory lifetime unlimited algorithm @DEFAULT_ALGORITHM@;
};
cdnskey no;
cds-digest-types { };
};
dnssec-policy "cds-cdnskey-alt" {
dnskey-ttl 3600;
keys {
ksk key-directory lifetime unlimited algorithm @DEFAULT_ALGORITHM@;
zsk key-directory lifetime unlimited algorithm @DEFAULT_ALGORITHM@;
};
cdnskey yes;
cds-digest-types { "sha-384"; };
};

View file

@ -50,7 +50,7 @@ for zn in default dnssec-keygen some-keys legacy-keys pregenerated \
rumoured rsasha256 rsasha512 ecdsa256 ecdsa384 \
dynamic dynamic-inline-signing inline-signing \
checkds-ksk checkds-doubleksk checkds-csk inherit unlimited \
keystore; do
keystore cds-cdnskey; do
setup "${zn}.kasp"
cp template.db.in "$zonefile"
done
@ -139,6 +139,17 @@ $SETTIME -s -g $O -k $R $Tpub -r $R $Tpub -d $H $Tpub "$KSK" >settime.out.$zone.
$SETTIME -s -g $O -k $R $Tpub -z $R $Tpub "$ZSK1" >settime.out.$zone.2 2>&1
$SETTIME -s -g $O -k $R $Tpub -z $R $Tpub "$ZSK2" >settime.out.$zone.2 2>&1
# CDS and CDNSKEY should be published.
zone="cds-cdnskey.kasp"
echo_i "setting up zone: $zone"
Tpub="now-7d"
Tact="now"
keytimes="-P ${Tpub} -A ${Tact}"
KSK=$($KEYGEN -a $DEFAULT_ALGORITHM -L 3600 -f KSK $keytimes $zone 2>keygen.out.$zone.1)
ZSK=$($KEYGEN -a $DEFAULT_ALGORITHM -L 3600 $keytimes $zone 2>keygen.out.$zone.2)
$SETTIME -s -g $O -k $R $Tpub -r $R $Tpub -d $H $Tpub "$KSK" >settime.out.$zone.1 2>&1
$SETTIME -s -g $O -k $R $Tpub -z $R $Tpub "$ZSK" >settime.out.$zone.2 2>&1
#
# Set up zones that are already signed.
#

View file

@ -175,7 +175,15 @@ def fips_properties(alg, bits=None):
def check_all(
server, zone, policy, ksks, zsks, manual_mode=False, zsk_missing=False, tsig=None
server,
zone,
policy,
ksks,
zsks,
manual_mode=False,
zsk_missing=False,
cdss=None,
tsig=None,
):
isctest.kasp.check_dnssecstatus(server, zone, ksks + zsks, policy=policy)
isctest.kasp.check_apex(
@ -183,6 +191,7 @@ def check_all(
zone,
ksks,
zsks,
cdss=cdss,
manual_mode=manual_mode,
zsk_missing=zsk_missing,
tsig=tsig,
@ -1067,6 +1076,44 @@ def test_kasp_dynamic(ns3):
assert f"zone_resigninc: zone {zone}/IN (unsigned): enter" not in "ns3/named.run"
def test_kasp_cds_cdnskey(ns3, default_algorithm):
# Zone: cds-cdnskey.kasp.
zone = "cds-cdnskey.kasp"
policy = "cds-cdnskey"
policy_keys = [
f"ksk unlimited {default_algorithm.number} {default_algorithm.bits} goal:omnipresent dnskey:omnipresent krrsig:omnipresent ds:rumoured",
f"zsk unlimited {default_algorithm.number} {default_algorithm.bits} goal:omnipresent dnskey:omnipresent zrrsig:omnipresent",
]
isctest.kasp.wait_keymgr_done(ns3, zone)
expected = isctest.kasp.policy_to_properties(ttl=3600, keys=policy_keys)
keys = isctest.kasp.keydir_to_keylist(zone, "ns3")
ksks = [k for k in keys if k.is_ksk()]
zsks = [k for k in keys if k.is_zsk()]
isctest.kasp.check_dnssec_verify(ns3, zone)
isctest.kasp.check_keys(zone, keys, expected)
check_all(ns3, zone, policy, ksks, zsks)
# Reconfig
shutil.copyfile("ns3/named-reconfig2.conf", "ns3/named-reconfig.conf")
with ns3.watch_log_from_here() as watcher:
ns3.rndc("reconfig")
watcher.wait_for_line(f"keymgr: {zone} done")
policy = "cds-cdnskey-no"
check_all(ns3, zone, policy, ksks, zsks, cdss=[])
# Reconfig, put CDS, CDNSKEY back
shutil.copyfile("ns3/named-reconfig3.conf", "ns3/named-reconfig.conf")
with ns3.watch_log_from_here() as watcher:
ns3.rndc("reconfig")
watcher.wait_for_line(f"keymgr: {zone} done")
policy = "cds-cdnskey-alt"
check_all(ns3, zone, policy, ksks, zsks, cdss=["CDNSKEY", "CDS (SHA-384)"])
def test_kasp_checkds(ns3, default_algorithm):
def wait_for_metadata():
return isctest.util.file_contents_contain(ksk.statefile, metadata)

View file

@ -1849,9 +1849,22 @@ add_cds(dns_dnsseckey_t *key, dns_rdata_t *keyrdata, const char *keystr,
DNS_LOGMODULE_DNSSEC, ISC_LOG_INFO,
"CDS (%s) for key %s is now published", algbuf,
keystr);
r = addrdata(&cdsrdata, diff, origin, ttl, mctx);
return addrdata(&cdsrdata, diff, origin, ttl, mctx);
}
return r;
return DNS_R_UNCHANGED;
}
static bool
contains_digest(dns_kasp_digestlist_t *digests, unsigned int digesttype) {
for (dns_kasp_digest_t *alg = ISC_LIST_HEAD(*digests); alg != NULL;
alg = ISC_LIST_NEXT(alg, link))
{
if (digesttype == alg->digest) {
return true;
}
}
return false;
}
static isc_result_t
@ -1877,9 +1890,9 @@ delete_cds(dns_dnsseckey_t *key, dns_rdata_t *keyrdata, const char *keystr,
DNS_LOGMODULE_DNSSEC, ISC_LOG_INFO,
"CDS (%s) for key %s is now deleted", algbuf,
keystr);
r = delrdata(&cdsrdata, diff, origin, cds->ttl, mctx);
return delrdata(&cdsrdata, diff, origin, cds->ttl, mctx);
}
return r;
return DNS_R_UNCHANGED;
}
isc_result_t
@ -1893,6 +1906,7 @@ dns_dnssec_syncupdate(dns_dnsseckeylist_t *keys, dns_dnsseckeylist_t *rmkeys,
dns_dnsseckey_t *key;
dns_ttl_t cdsttl = ttl;
dns_ttl_t cdnskeyttl = ttl;
bool changed = false;
REQUIRE(digests != NULL);
REQUIRE(keys != NULL);
@ -1923,9 +1937,15 @@ dns_dnssec_syncupdate(dns_dnsseckeylist_t *keys, dns_dnsseckeylist_t *rmkeys,
for (dns_kasp_digest_t *alg = ISC_LIST_HEAD(*digests);
alg != NULL; alg = ISC_LIST_NEXT(alg, link))
{
CHECK(add_cds(key, &cdnskeyrdata,
(const char *)keystr, cds,
alg->digest, cdsttl, diff, mctx));
result = add_cds(key, &cdnskeyrdata,
(const char *)keystr, cds,
alg->digest, cdsttl, diff,
mctx);
if (result == ISC_R_SUCCESS) {
changed = true;
} else if (result != DNS_R_UNCHANGED) {
goto cleanup;
}
}
if (gencdnskey &&
@ -1939,41 +1959,64 @@ dns_dnssec_syncupdate(dns_dnsseckeylist_t *keys, dns_dnsseckeylist_t *rmkeys,
keystr);
RETERR(addrdata(&cdnskeyrdata, diff, origin,
cdnskeyttl, mctx));
changed = true;
}
}
if (syncdelete(key->key, now)) {
if (dns_rdataset_isassociated(cds)) {
char keystr[DST_KEY_FORMATSIZE];
dst_key_format(key->key, keystr, sizeof(keystr));
if (dns_rdataset_isassociated(cds)) {
/* Delete all possible CDS records */
delete_cds(key, &cdnskeyrdata,
(const char *)keystr, cds,
DNS_DSDIGEST_SHA1, diff, mctx);
delete_cds(key, &cdnskeyrdata,
(const char *)keystr, cds,
DNS_DSDIGEST_SHA256, diff, mctx);
delete_cds(key, &cdnskeyrdata,
(const char *)keystr, cds,
DNS_DSDIGEST_SHA384, diff, mctx);
}
if (dns_rdataset_isassociated(cdnskey)) {
if (exists(cdnskey, &cdnskeyrdata)) {
isc_log_write(dns_lctx,
DNS_LOGCATEGORY_GENERAL,
DNS_LOGMODULE_DNSSEC,
ISC_LOG_INFO,
"CDNSKEY for key %s is "
"now deleted",
keystr);
RETERR(delrdata(&cdnskeyrdata, diff,
origin, cdnskey->ttl,
mctx));
/* Delete all possible CDS records */
for (dns_dsdigest_t digest = DNS_DSDIGEST_SHA1;
digest < DNS_DSDIGEST_TOTAL; digest++)
{
if (syncdelete(key->key, now) ||
!contains_digest(digests, digest))
{
result = delete_cds(
key, &cdnskeyrdata,
(const char *)keystr, cds,
digest, diff, mctx);
switch (result) {
case ISC_R_SUCCESS:
changed = true;
break;
case DNS_R_UNCHANGED:
case ISC_R_NOTIMPLEMENTED:
/*
* Either the digest is not
* supported and we cannot
* construct the CDS for it, or
* the CDS with this digest is
* not present in the CDS RRset.
*/
break;
default:
goto cleanup;
}
}
}
}
if (dns_rdataset_isassociated(cdnskey) &&
exists(cdnskey, &cdnskeyrdata))
{
if (syncdelete(key->key, now) || !gencdnskey) {
char keystr[DST_KEY_FORMATSIZE];
dst_key_format(key->key, keystr,
sizeof(keystr));
isc_log_write(
dns_lctx, DNS_LOGCATEGORY_GENERAL,
DNS_LOGMODULE_DNSSEC, ISC_LOG_INFO,
"CDNSKEY for key %s is now deleted",
keystr);
delrdata(&cdnskeyrdata, diff, origin,
cdnskey->ttl, mctx);
changed = true;
}
}
}
if (!dns_rdataset_isassociated(cds) &&
@ -1998,12 +2041,30 @@ dns_dnssec_syncupdate(dns_dnsseckeylist_t *keys, dns_dnsseckeylist_t *rmkeys,
&cdnskeyrdata));
if (dns_rdataset_isassociated(cds)) {
delete_cds(key, &cdnskeyrdata, (const char *)keystr,
cds, DNS_DSDIGEST_SHA1, diff, mctx);
delete_cds(key, &cdnskeyrdata, (const char *)keystr,
cds, DNS_DSDIGEST_SHA256, diff, mctx);
delete_cds(key, &cdnskeyrdata, (const char *)keystr,
cds, DNS_DSDIGEST_SHA384, diff, mctx);
for (dns_dsdigest_t digest = DNS_DSDIGEST_SHA1;
digest < DNS_DSDIGEST_TOTAL; digest++)
{
result = delete_cds(key, &cdnskeyrdata,
(const char *)keystr, cds,
digest, diff, mctx);
switch (result) {
case ISC_R_SUCCESS:
changed = true;
break;
case DNS_R_UNCHANGED:
case ISC_R_NOTIMPLEMENTED:
/*
* Either the digest is not
* supported and we cannot
* construct the CDS for it, or
* the CDS with this digest is
* not present in the CDS RRset.
*/
break;
default:
goto cleanup;
}
}
}
if (dns_rdataset_isassociated(cdnskey)) {
@ -2015,11 +2076,15 @@ dns_dnssec_syncupdate(dns_dnsseckeylist_t *keys, dns_dnsseckeylist_t *rmkeys,
keystr);
RETERR(delrdata(&cdnskeyrdata, diff, origin,
cdnskey->ttl, mctx));
changed = true;
}
}
}
result = ISC_R_SUCCESS;
if (changed) {
return ISC_R_SUCCESS;
}
return DNS_R_UNCHANGED;
cleanup:
return result;
@ -2036,6 +2101,7 @@ dns_dnssec_syncdelete(dns_rdataset_t *cds, dns_rdataset_t *cdnskey,
dns_rdata_t cds_delete = DNS_RDATA_INIT;
dns_rdata_t cdnskey_delete = DNS_RDATA_INIT;
isc_region_t r;
bool changed = false;
r.base = keybuf;
r.length = sizeof(keybuf);
@ -2058,6 +2124,7 @@ dns_dnssec_syncdelete(dns_rdataset_t *cds, dns_rdataset_t *cdnskey,
"published",
namebuf);
RETERR(addrdata(&cds_delete, diff, origin, ttl, mctx));
changed = true;
}
} else {
if (dns_rdataset_isassociated(cds) && exists(cds, &cds_delete))
@ -2069,6 +2136,7 @@ dns_dnssec_syncdelete(dns_rdataset_t *cds, dns_rdataset_t *cdnskey,
namebuf);
RETERR(delrdata(&cds_delete, diff, origin, cds->ttl,
mctx));
changed = true;
}
}
@ -2083,6 +2151,7 @@ dns_dnssec_syncdelete(dns_rdataset_t *cds, dns_rdataset_t *cdnskey,
namebuf);
RETERR(addrdata(&cdnskey_delete, diff, origin, ttl,
mctx));
changed = true;
}
} else {
if (dns_rdataset_isassociated(cdnskey) &&
@ -2095,10 +2164,14 @@ dns_dnssec_syncdelete(dns_rdataset_t *cds, dns_rdataset_t *cdnskey,
namebuf);
RETERR(delrdata(&cdnskey_delete, diff, origin,
cdnskey->ttl, mctx));
changed = true;
}
}
return ISC_R_SUCCESS;
if (changed) {
return ISC_R_SUCCESS;
}
return DNS_R_UNCHANGED;
}
/*

View file

@ -22,6 +22,7 @@
#define DNS_DSDIGEST_SHA256 (2)
#define DNS_DSDIGEST_GOST (3)
#define DNS_DSDIGEST_SHA384 (4)
#define DNS_DSDIGEST_TOTAL (5)
#define DNS_DSDIGEST_MAX (255)

View file

@ -22797,7 +22797,7 @@ zone_rekey(dns_zone_t *zone) {
result = dns_dnssec_syncupdate(&dnskeys, &rmkeys, &cdsset,
&cdnskeyset, now, &digests,
cdnskeypub, ttl, &diff, mctx);
if (result != ISC_R_SUCCESS) {
if (result != ISC_R_SUCCESS && result != DNS_R_UNCHANGED) {
dnssec_log(zone, ISC_LOG_ERROR,
"zone_rekey:couldn't update CDS/CDNSKEY: %s",
isc_result_totext(result));
@ -22835,7 +22835,7 @@ zone_rekey(dns_zone_t *zone) {
result = dns_dnssec_syncdelete(
&cdsset, &cdnskeyset, &zone->origin, zone->rdclass, ttl,
&diff, mctx, cdsdel, cdnskeydel);
if (result != ISC_R_SUCCESS) {
if (result != ISC_R_SUCCESS && result != DNS_R_UNCHANGED) {
dnssec_log(zone, ISC_LOG_ERROR,
"zone_rekey:couldn't update CDS/CDNSKEY "
"DELETE records: %s",