From 75bfd8820d40d1fafbde0c81ae71715ece45cf37 Mon Sep 17 00:00:00 2001 From: Matthijs Mekking Date: Wed, 17 Jun 2026 17:29:51 +0200 Subject: [PATCH 1/3] Add system test for reconfiguring CDS/CDNSKEY When on an 'rndc reconfig' the DNSSEC policy changes such that it changes the expected CDNSKEY/CDS records in the zone, the RRset should be updated accordingly. Add a test case where we reconfigure a zone with a policy such that these records should be removed, and on a second reconfigure add them back again. Note the test deliberately adds a different CDS digest on the second reconfigure. (cherry picked from commit 89ebe61ad3f61c9c66b54157512b59b8c30717d2) --- .../system/kasp/ns3/named-reconfig.conf.j2 | 6 +++ .../system/kasp/ns3/named-reconfig2.conf.j2 | 6 +++ .../system/kasp/ns3/named-reconfig3.conf.j2 | 6 +++ bin/tests/system/kasp/ns3/named.conf.j2 | 1 + .../kasp/ns3/policies/kasp-fips.conf.j2 | 33 +++++++++++++ bin/tests/system/kasp/ns3/setup.sh | 13 ++++- bin/tests/system/kasp/tests_kasp.py | 49 ++++++++++++++++++- 7 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 bin/tests/system/kasp/ns3/named-reconfig.conf.j2 create mode 100644 bin/tests/system/kasp/ns3/named-reconfig2.conf.j2 create mode 100644 bin/tests/system/kasp/ns3/named-reconfig3.conf.j2 diff --git a/bin/tests/system/kasp/ns3/named-reconfig.conf.j2 b/bin/tests/system/kasp/ns3/named-reconfig.conf.j2 new file mode 100644 index 0000000000..7a3504a6b8 --- /dev/null +++ b/bin/tests/system/kasp/ns3/named-reconfig.conf.j2 @@ -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"; +}; diff --git a/bin/tests/system/kasp/ns3/named-reconfig2.conf.j2 b/bin/tests/system/kasp/ns3/named-reconfig2.conf.j2 new file mode 100644 index 0000000000..2caeaeca08 --- /dev/null +++ b/bin/tests/system/kasp/ns3/named-reconfig2.conf.j2 @@ -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"; +}; diff --git a/bin/tests/system/kasp/ns3/named-reconfig3.conf.j2 b/bin/tests/system/kasp/ns3/named-reconfig3.conf.j2 new file mode 100644 index 0000000000..21a18ba1c9 --- /dev/null +++ b/bin/tests/system/kasp/ns3/named-reconfig3.conf.j2 @@ -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"; +}; diff --git a/bin/tests/system/kasp/ns3/named.conf.j2 b/bin/tests/system/kasp/ns3/named.conf.j2 index 0974e1d270..46c211dcef 100644 --- a/bin/tests/system/kasp/ns3/named.conf.j2 +++ b/bin/tests/system/kasp/ns3/named.conf.j2 @@ -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"; diff --git a/bin/tests/system/kasp/ns3/policies/kasp-fips.conf.j2 b/bin/tests/system/kasp/ns3/policies/kasp-fips.conf.j2 index 68f932bcf1..9f084e417f 100644 --- a/bin/tests/system/kasp/ns3/policies/kasp-fips.conf.j2 +++ b/bin/tests/system/kasp/ns3/policies/kasp-fips.conf.j2 @@ -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"; }; +}; diff --git a/bin/tests/system/kasp/ns3/setup.sh b/bin/tests/system/kasp/ns3/setup.sh index 756c0af19c..cf55c66634 100644 --- a/bin/tests/system/kasp/ns3/setup.sh +++ b/bin/tests/system/kasp/ns3/setup.sh @@ -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. # diff --git a/bin/tests/system/kasp/tests_kasp.py b/bin/tests/system/kasp/tests_kasp.py index 13b711b283..c4ce8f738c 100644 --- a/bin/tests/system/kasp/tests_kasp.py +++ b/bin/tests/system/kasp/tests_kasp.py @@ -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) From dbb0dd5088bc08b3eb859207eb619ee1f68bf3ee Mon Sep 17 00:00:00 2001 From: Matthijs Mekking Date: Wed, 17 Jun 2026 17:34:42 +0200 Subject: [PATCH 2/3] Remove CDs/CDNSKEY records on reconfig When adding to dnssec-policy: cdnskey no; cds-digest-types { }; and then reconfig the server, named must remove existing CDS and CDNSKEY records. Note this already worked when adding CDS digest, or setting 'cdnskey yes;', but not when digests were removed from the list, or when setting 'cdnskey no;'. (cherry picked from commit 1c4c2cba8b2cf56f241c831f74b1f4f381bc45a3) --- lib/dns/dnssec.c | 155 ++++++++++++++++++++++++++++----------- lib/dns/include/dns/ds.h | 1 + lib/dns/zone.c | 4 +- 3 files changed, 117 insertions(+), 43 deletions(-) diff --git a/lib/dns/dnssec.c b/lib/dns/dnssec.c index c36ea9d10f..aa664b9187 100644 --- a/lib/dns/dnssec.c +++ b/lib/dns/dnssec.c @@ -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; } /* diff --git a/lib/dns/include/dns/ds.h b/lib/dns/include/dns/ds.h index 649ca80065..b63cfccdee 100644 --- a/lib/dns/include/dns/ds.h +++ b/lib/dns/include/dns/ds.h @@ -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) diff --git a/lib/dns/zone.c b/lib/dns/zone.c index 655da2999f..8137923382 100644 --- a/lib/dns/zone.c +++ b/lib/dns/zone.c @@ -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", From 4bed09f3c474642892b4264f8a46da14521eabd5 Mon Sep 17 00:00:00 2001 From: Matthijs Mekking Date: Thu, 18 Jun 2026 10:42:10 +0200 Subject: [PATCH 3/3] Don't rely on smart signing in cds system test With dnssec-signzone smart-signing (-S), the CDS and CDSNKEY are derived from the key timing metadata and the configuration options (-G). The test has specific test cases that smart signing (with the fix) interferes with. Therefor, disable smart-signing in the cds system test. (cherry picked from commit fdf74636e207729584bcc0b291b5134e736b13fe) --- bin/tests/system/cds/setup.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bin/tests/system/cds/setup.sh b/bin/tests/system/cds/setup.sh index 27474bcc0a..842c9c1402 100644 --- a/bin/tests/system/cds/setup.sh +++ b/bin/tests/system/cds/setup.sh @@ -87,8 +87,9 @@ sed 's/ add \(.*\) IN DS / add \1 3600 IN DS /' 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 <