]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Convert enable dnssec test case to pytest
authorMatthijs Mekking <matthijs@isc.org>
Tue, 18 Mar 2025 09:34:53 +0000 (10:34 +0100)
committerMatthijs Mekking <matthijs@isc.org>
Mon, 2 Jun 2025 09:21:06 +0000 (09:21 +0000)
Move the 'enable-dnssec' to the rollover test dir and convert to pytest.

This requires new test functionality to check that "CDS is published"
messages are logged (or prohibited).

The setup part is slightly adapted such that it no longer needs to
set the '-P sync' value in most cases (this is then set by 'named'),
and to adjust for the inappropriate safety intervals fix.

bin/tests/system/isctest/kasp.py
bin/tests/system/isctest/vars/algorithms.py
bin/tests/system/kasp/ns3/named-fips.conf.in
bin/tests/system/kasp/ns3/policies/autosign.conf.in
bin/tests/system/kasp/ns3/setup.sh
bin/tests/system/kasp/tests.sh
bin/tests/system/rollover/ns3/kasp.conf.j2
bin/tests/system/rollover/ns3/named.conf.j2
bin/tests/system/rollover/ns3/setup.sh
bin/tests/system/rollover/tests_rollover.py

index 35d8c6e82a9d01b187a03f13b607769fa455d161..5beb8f26323d3557c12cc5710955ce7cf1d150f5 100644 (file)
@@ -25,6 +25,7 @@ import dns.tsig
 import isctest.log
 import isctest.query
 import isctest.util
+from isctest.vars.algorithms import Algorithm, ALL_ALGORITHMS_BY_NUM
 
 DEFAULT_TTL = 300
 
@@ -403,6 +404,11 @@ class Key:
     def is_zsk(self) -> bool:
         return self.get_metadata("ZSK") == "yes"
 
+    @property
+    def algorithm(self) -> Algorithm:
+        num = int(self.get_metadata("Algorithm"))
+        return ALL_ALGORITHMS_BY_NUM[num]
+
     def dnskey_equals(self, value, cdnskey=False):
         dnskey = value.split()
 
@@ -955,6 +961,19 @@ def check_cds(rrset, keys):
     assert numcds == len(cdss)
 
 
+def check_cdslog(server, zone, key, substr):
+    with server.watch_log_from_start() as watcher:
+        watcher.wait_for_line(
+            f"{substr} for key {zone}/{key.algorithm.name}/{key.tag} is now published"
+        )
+
+
+def check_cdslog_prohibit(server, zone, key, substr):
+    server.log.prohibit(
+        f"{substr} for key {zone}/{key.algorithm.name}/{key.tag} is now published"
+    )
+
+
 def _query_rrset(server, fqdn, qtype, tsig=None):
     response = _query(server, fqdn, qtype, tsig=tsig)
     assert response.rcode() == dns.rcode.NOERROR
index 26bcc579f8bc36ca24ac4c034f552bc5b06f01de..446ab09d695a1e38c4c31487ebb8a89fd90e7c2b 100644 (file)
@@ -90,6 +90,8 @@ ALL_ALGORITHMS = [
     ED448,
 ]
 
+ALL_ALGORITHMS_BY_NUM = {alg.number: alg for alg in ALL_ALGORITHMS}
+
 ALGORITHM_SETS = {
     "stable": AlgorithmSet(
         default=ECDSAP256SHA256, alternative=RSASHA256, disabled=ECDSAP384SHA384
index 8368c4822e738b8401bdd8925c5e46842d5bf768..dbbc6e0f37ef5c51a922615e7fdffd05c0bbb875 100644 (file)
@@ -324,30 +324,6 @@ zone "zsk-retired.autosign" {
        dnssec-policy "autosign";
 };
 
-/*
- * Zones for testing enabling DNSSEC.
- */
-zone "step1.enable-dnssec.autosign" {
-       type primary;
-       file "step1.enable-dnssec.autosign.db";
-       dnssec-policy "enable-dnssec";
-};
-zone "step2.enable-dnssec.autosign" {
-       type primary;
-       file "step2.enable-dnssec.autosign.db";
-       dnssec-policy "enable-dnssec";
-};
-zone "step3.enable-dnssec.autosign" {
-       type primary;
-       file "step3.enable-dnssec.autosign.db";
-       dnssec-policy "enable-dnssec";
-};
-zone "step4.enable-dnssec.autosign" {
-       type primary;
-       file "step4.enable-dnssec.autosign.db";
-       dnssec-policy "enable-dnssec";
-};
-
 /*
  * Zones for testing ZSK Pre-Publication steps.
  */
index bc3b7e11ddb6cbe4acc8235404e59ddbf55db72b..062885ad241b89812ec0adb25b3eada18948106f 100644 (file)
@@ -25,26 +25,6 @@ dnssec-policy "autosign" {
        };
 };
 
-dnssec-policy "enable-dnssec" {
-
-       signatures-refresh P1W;
-       signatures-validity P2W;
-       signatures-validity-dnskey P2W;
-
-       dnskey-ttl 300;
-       max-zone-ttl PT12H;
-       zone-propagation-delay PT5M;
-       retire-safety PT20M;
-       publish-safety PT5M;
-
-       parent-propagation-delay 1h;
-       parent-ds-ttl 2h;
-
-       keys {
-               csk lifetime unlimited algorithm @DEFAULT_ALGORITHM_NUMBER@;
-       };
-};
-
 dnssec-policy "zsk-prepub" {
 
        signatures-refresh P1W;
index 7eeb2488384b6b6beaa11964408b556cfbcdc2d1..827bca18c0d876ff6d6e38c67767e0616e27bd6a 100644 (file)
@@ -281,75 +281,6 @@ oldtimes="-P $T2 -A $T2 -I $T1 -D $T1"
 OLD=$($KEYGEN -a $DEFAULT_ALGORITHM -L 300 $oldtimes $zone 2>keygen.out.$zone.3)
 $SETTIME -s -g $H -k $H $T1 -z $H $T1 "$OLD" >settime.out.$zone.3 2>&1
 
-#
-# The zones at enable-dnssec.autosign represent the various steps of the
-# initial signing of a zone.
-#
-
-# Step 1:
-# This is an unsigned zone and named should perform the initial steps of
-# introducing the DNSSEC records in the right order.
-setup step1.enable-dnssec.autosign
-cp template.db.in $zonefile
-
-# Step 2:
-# The DNSKEY has been published long enough to become OMNIPRESENT.
-setup step2.enable-dnssec.autosign
-# DNSKEY TTL:             300 seconds
-# zone-propagation-delay: 5 minutes (300 seconds)
-# publish-safety:         5 minutes (300 seconds)
-# Total:                  900 seconds
-TpubN="now-900s"
-# RRSIG TTL:              12 hour (43200 seconds)
-# zone-propagation-delay: 5 minutes (300 seconds)
-# Already passed time:    -900 seconds
-# Total:                  42600 seconds
-TsbmN="now+42600s"
-keytimes="-P ${TpubN} -P sync ${TsbmN} -A ${TpubN}"
-CSK=$($KEYGEN -k enable-dnssec -l policies/autosign.conf $keytimes $zone 2>keygen.out.$zone.1)
-$SETTIME -s -g $O -k $R $TpubN -r $R $TpubN -d $H $TpubN -z $R $TpubN "$CSK" >settime.out.$zone.1 2>&1
-cat template.db.in "${CSK}.key" >"$infile"
-private_type_record $zone $DEFAULT_ALGORITHM_NUMBER "$CSK" >>"$infile"
-cp $infile $zonefile
-$SIGNER -S -z -x -s now-1h -e now+30d -o $zone -O raw -f "${zonefile}.signed" $infile >signer.out.$zone.1 2>&1
-
-# Step 3:
-# The zone signatures have been published long enough to become OMNIPRESENT.
-setup step3.enable-dnssec.autosign
-# Passed time since publications: 42600 + 900 = 43500 seconds.
-TpubN="now-43500s"
-# The key is secure for using in chain of trust when the DNSKEY is OMNIPRESENT.
-TcotN="now-42600s"
-# We can submit the DS now.
-TsbmN="now"
-keytimes="-P ${TpubN} -P sync ${TsbmN} -A ${TpubN}"
-CSK=$($KEYGEN -k enable-dnssec -l policies/autosign.conf $keytimes $zone 2>keygen.out.$zone.1)
-$SETTIME -s -g $O -k $O $TcotN -r $O $TcotN -d $H $TpubN -z $R $TpubN "$CSK" >settime.out.$zone.1 2>&1
-cat template.db.in "${CSK}.key" >"$infile"
-private_type_record $zone $DEFAULT_ALGORITHM_NUMBER "$CSK" >>"$infile"
-cp $infile $zonefile
-$SIGNER -S -z -x -s now-1h -e now+30d -o $zone -O raw -f "${zonefile}.signed" $infile >signer.out.$zone.1 2>&1
-
-# Step 4:
-# The DS has been submitted long enough ago to become OMNIPRESENT.
-setup step4.enable-dnssec.autosign
-# DS TTL:                    2 hour (7200 seconds)
-# parent-propagation-delay:  1 hour (3600 seconds)
-# retire-safety:             20 minutes (1200 seconds)
-# Total aditional time:      12000 seconds
-# 44700 + 12000 = 56700
-TpubN="now-56700s"
-# 43800 + 12000 = 55800
-TcotN="now-55800s"
-TsbmN="now-12000s"
-keytimes="-P ${TpubN} -P sync ${TsbmN} -A ${TpubN}"
-CSK=$($KEYGEN -k enable-dnssec -l policies/autosign.conf $keytimes $zone 2>keygen.out.$zone.1)
-$SETTIME -s -g $O -P ds $TsbmN -k $O $TcotN -r $O $TcotN -d $R $TsbmN -z $O $TsbmN "$CSK" >settime.out.$zone.1 2>&1
-cat template.db.in "${CSK}.key" >"$infile"
-private_type_record $zone $DEFAULT_ALGORITHM_NUMBER "$CSK" >>"$infile"
-cp $infile $zonefile
-$SIGNER -S -z -x -s now-1h -e now+30d -o $zone -O raw -f "${zonefile}.signed" $infile >signer.out.$zone.1 2>&1
-
 #
 # The zones at zsk-prepub.autosign represent the various steps of a ZSK
 # Pre-Publication rollover.
index b83b6a16d6fad3278d0a3f290af4ca4f2b44549c..ae5f3b9774ff55cd5458b714c3d841f3626f3708 100644 (file)
@@ -206,55 +206,6 @@ set_keytimes_autosign_policy() {
   set_addkeytime "KEY2" "REMOVED" "${retired}" 695100
 }
 
-#
-# Testing DNSSEC introduction.
-#
-
-#
-# Zone: step1.enable-dnssec.autosign.
-#
-set_zone "step1.enable-dnssec.autosign"
-set_policy "enable-dnssec" "1" "300"
-set_server "ns3" "10.53.0.3"
-# Key properties.
-key_clear "KEY1"
-set_keyrole "KEY1" "csk"
-set_keylifetime "KEY1" "0"
-set_keyalgorithm "KEY1" "$DEFAULT_ALGORITHM_NUMBER" "$DEFAULT_ALGORITHM" "$DEFAULT_BITS"
-set_keysigning "KEY1" "yes"
-set_zonesigning "KEY1" "yes"
-# The DNSKEY and signatures are introduced first, the DS remains hidden.
-set_keystate "KEY1" "GOAL" "omnipresent"
-set_keystate "KEY1" "STATE_DNSKEY" "rumoured"
-set_keystate "KEY1" "STATE_KRRSIG" "rumoured"
-set_keystate "KEY1" "STATE_ZRRSIG" "rumoured"
-set_keystate "KEY1" "STATE_DS" "hidden"
-# This policy lists only one key (CSK).
-key_clear "KEY2"
-key_clear "KEY3"
-key_clear "KEY4"
-
-check_keys
-check_dnssecstatus "$SERVER" "$POLICY" "$ZONE"
-
-# Set expected key times:
-# - The first key is immediately published and activated.
-created=$(key_get KEY1 CREATED)
-set_keytime "KEY1" "PUBLISHED" "${created}"
-set_keytime "KEY1" "ACTIVE" "${created}"
-# - The DS can be published if the DNSKEY and RRSIG records are
-#   OMNIPRESENT.  This happens after max-zone-ttl (12h) plus
-#   plus zone-propagation-delay (5m) =
-#   43200 + 300 = 43500.
-set_addkeytime "KEY1" "SYNCPUBLISH" "${created}" 43500
-# - Key lifetime is unlimited, so not setting RETIRED and REMOVED.
-
-# Various signing policy checks.
-check_keytimes
-check_apex
-check_subdomain
-dnssec_verify
-
 _check_next_key_event() {
   _expect=$1
 
@@ -290,110 +241,6 @@ check_next_key_event() {
 
 }
 
-# Next key event is when the DNSKEY RRset becomes OMNIPRESENT: DNSKEY TTL plus
-# publish safety plus the zone propagation delay: 900 seconds.
-check_next_key_event 900
-
-#
-# Zone: step2.enable-dnssec.autosign.
-#
-set_zone "step2.enable-dnssec.autosign"
-set_policy "enable-dnssec" "1" "300"
-set_server "ns3" "10.53.0.3"
-# The DNSKEY is omnipresent, but the zone signatures not yet.
-# Thus, the DS remains hidden.
-set_keystate "KEY1" "STATE_DNSKEY" "omnipresent"
-set_keystate "KEY1" "STATE_KRRSIG" "omnipresent"
-
-# Various signing policy checks.
-check_keys
-check_dnssecstatus "$SERVER" "$POLICY" "$ZONE"
-
-# Set expected key times:
-# - The key was published and activated 900 seconds ago (with settime).
-created=$(key_get KEY1 CREATED)
-set_addkeytime "KEY1" "PUBLISHED" "${created}" -900
-set_addkeytime "KEY1" "ACTIVE" "${created}" -900
-set_addkeytime "KEY1" "SYNCPUBLISH" "${created}" 42600
-
-# Continue signing policy checks.
-check_keytimes
-check_apex
-check_subdomain
-dnssec_verify
-
-# Next key event is when the zone signatures become OMNIPRESENT: max-zone-ttl
-# plus zone propagation delay plus retire safety minus the already elapsed
-# 900 seconds: 12h + 300s + 20m - 900 = 43500 - 900 = 42600 seconds
-check_next_key_event 42600
-
-#
-# Zone: step3.enable-dnssec.autosign.
-#
-set_zone "step3.enable-dnssec.autosign"
-set_policy "enable-dnssec" "1" "300"
-set_server "ns3" "10.53.0.3"
-# All signatures should be omnipresent, so the DS can be submitted.
-set_keystate "KEY1" "STATE_ZRRSIG" "omnipresent"
-set_keystate "KEY1" "STATE_DS" "rumoured"
-
-# Various signing policy checks.
-check_keys
-check_dnssecstatus "$SERVER" "$POLICY" "$ZONE"
-
-# Set expected key times:
-# - The key was published and activated 43500 seconds ago (with settime).
-created=$(key_get KEY1 CREATED)
-set_addkeytime "KEY1" "PUBLISHED" "${created}" -43500
-set_addkeytime "KEY1" "ACTIVE" "${created}" -43500
-set_keytime "KEY1" "SYNCPUBLISH" "${created}"
-
-# Continue signing policy checks.
-check_keytimes
-check_apex
-check_subdomain
-dnssec_verify
-# Check that CDS publication is logged.
-check_cdslog "$DIR" "$ZONE" KEY1
-
-# The DS can be introduced. We ignore any parent registration delay, so set
-# the DS publish time to now.
-rndc_checkds "$SERVER" "$DIR" KEY1 "now" "published" "$ZONE"
-# Next key event is when the DS can move to the OMNIPRESENT state.  This occurs
-# when the parent propagation delay have passed, plus the DS TTL and retire
-# safety delay:  1h + 2h = 3h = 10800 seconds
-check_next_key_event 10800
-
-#
-# Zone: step4.enable-dnssec.autosign.
-#
-set_zone "step4.enable-dnssec.autosign"
-set_policy "enable-dnssec" "1" "300"
-set_server "ns3" "10.53.0.3"
-# The DS is omnipresent.
-set_keystate "KEY1" "STATE_DS" "omnipresent"
-
-# Various signing policy checks.
-check_keys
-check_dnssecstatus "$SERVER" "$POLICY" "$ZONE"
-
-# Set expected key times:
-# - The key was published and activated 56700 seconds ago (with settime).
-created=$(key_get KEY1 CREATED)
-set_addkeytime "KEY1" "PUBLISHED" "${created}" -56700
-set_addkeytime "KEY1" "ACTIVE" "${created}" -56700
-set_addkeytime "KEY1" "SYNCPUBLISH" "${created}" -12000
-
-# Continue signing policy checks.
-check_keytimes
-check_apex
-check_subdomain
-dnssec_verify
-
-# Next key event is never, the zone dnssec-policy has been established. So we
-# fall back to the default loadkeys interval.
-check_next_key_event 3600
-
 #
 # Testing ZSK Pre-Publication rollover.
 #
index 2ab26877f0e6b5d7575403d98a886aebdaf7d679..f432cfc6c01504fd799ed28be1761ce6ad9052d0 100644 (file)
@@ -29,3 +29,22 @@ dnssec-policy "multisigner-model2" {
                zsk key-directory lifetime unlimited algorithm @DEFAULT_ALGORITHM@ tag-range 32768 65535;
        };
 };
+
+dnssec-policy "enable-dnssec" {
+       signatures-refresh P1W;
+       signatures-validity P2W;
+       signatures-validity-dnskey P2W;
+
+       dnskey-ttl 300;
+       max-zone-ttl PT12H;
+       zone-propagation-delay PT5M;
+       retire-safety PT20M;
+       publish-safety PT5M;
+
+       parent-propagation-delay 1h;
+       parent-ds-ttl 2h;
+
+       keys {
+               csk lifetime unlimited algorithm @DEFAULT_ALGORITHM_NUMBER@;
+       };
+};
index 573865d1b9658653d372c6910ff9398110913950..c65404a8ca5dad51e33eadf227e2339644a17026 100644 (file)
@@ -67,3 +67,27 @@ zone "single-to-multisigner.kasp" {
         dnssec-policy "multisigner-model2";
         allow-update { any; };
 };
+
+/*
+ * Zones for testing enabling DNSSEC.
+ */
+zone "step1.enable-dnssec.autosign" {
+       type primary;
+       file "step1.enable-dnssec.autosign.db";
+       dnssec-policy "enable-dnssec";
+};
+zone "step2.enable-dnssec.autosign" {
+       type primary;
+       file "step2.enable-dnssec.autosign.db";
+       dnssec-policy "enable-dnssec";
+};
+zone "step3.enable-dnssec.autosign" {
+       type primary;
+       file "step3.enable-dnssec.autosign.db";
+       dnssec-policy "enable-dnssec";
+};
+zone "step4.enable-dnssec.autosign" {
+       type primary;
+       file "step4.enable-dnssec.autosign.db";
+       dnssec-policy "enable-dnssec";
+};
index 969b6a7e6c126d02466afc176d0a63917ab19cb9..ac6a47924cf83134a3f65132e4a60d0c8fa81a98 100644 (file)
@@ -79,3 +79,64 @@ cat template.db.in "${KSK}.key" "${ZSK}.key" >"$infile"
 $SIGNER -PS -z -x -s now-2w -e now-1mi -o $zone -f "${zonefile}" $infile >signer.out.$zone.1 2>&1
 echo "Lifetime: 0" >>"${KSK}".state
 echo "Lifetime: 0" >>"${ZSK}".state
+
+#
+# The zones at enable-dnssec.autosign represent the various steps of the
+# initial signing of a zone.
+#
+
+# Step 1:
+# This is an unsigned zone and named should perform the initial steps of
+# introducing the DNSSEC records in the right order.
+setup step1.enable-dnssec.autosign
+cp template.db.in $zonefile
+
+# Step 2:
+# The DNSKEY has been published long enough to become OMNIPRESENT.
+setup step2.enable-dnssec.autosign
+# DNSKEY TTL:             300 seconds
+# zone-propagation-delay: 5 minutes (300 seconds)
+# publish-safety:         5 minutes (300 seconds)
+# Total:                  900 seconds
+TpubN="now-900s"
+keytimes="-P ${TpubN} -A ${TpubN}"
+CSK=$($KEYGEN -k enable-dnssec -l kasp.conf $keytimes $zone 2>keygen.out.$zone.1)
+$SETTIME -s -g $O -k $R $TpubN -r $R $TpubN -d $H $TpubN -z $R $TpubN "$CSK" >settime.out.$zone.1 2>&1
+cat template.db.in "${CSK}.key" >"$infile"
+private_type_record $zone $DEFAULT_ALGORITHM_NUMBER "$CSK" >>"$infile"
+cp $infile $zonefile
+$SIGNER -S -z -x -s now-1h -e now+30d -o $zone -O raw -f "${zonefile}.signed" $infile >signer.out.$zone.1 2>&1
+
+# Step 3:
+# The zone signatures have been published long enough to become OMNIPRESENT.
+setup step3.enable-dnssec.autosign
+# Passed time since publication:
+# max-zone-ttl:           12 hours (43200 seconds)
+# zone-propagation-delay: 5 minutes (300 seconds)
+TpubN="now-43500s"
+# We can submit the DS now.
+keytimes="-P ${TpubN} -A ${TpubN}"
+CSK=$($KEYGEN -k enable-dnssec -l kasp.conf $keytimes $zone 2>keygen.out.$zone.1)
+$SETTIME -s -g $O -k $O $TpubN -r $O $TpubN -d $H $TpubN -z $R $TpubN "$CSK" >settime.out.$zone.1 2>&1
+cat template.db.in "${CSK}.key" >"$infile"
+private_type_record $zone $DEFAULT_ALGORITHM_NUMBER "$CSK" >>"$infile"
+cp $infile $zonefile
+$SIGNER -S -z -x -s now-1h -e now+30d -o $zone -O raw -f "${zonefile}.signed" $infile >signer.out.$zone.1 2>&1
+
+# Step 4:
+# The DS has been submitted long enough ago to become OMNIPRESENT.
+setup step4.enable-dnssec.autosign
+# DS TTL:                    2 hour (7200 seconds)
+# parent-propagation-delay:  1 hour (3600 seconds)
+# Total aditional time:      10800 seconds
+# 43500 + 10800 = 54300
+TpubN="now-54300s"
+TsbmN="now-10800s"
+keytimes="-P ${TpubN} -A ${TpubN} -P sync ${TsbmN}"
+CSK=$($KEYGEN -k enable-dnssec -l kasp.conf $keytimes $zone 2>keygen.out.$zone.1)
+$SETTIME -s -g $O -P ds $TsbmN -k $O $TpubN -r $O $TpubN -d $R $TpubN -z $O $TsbmN "$CSK" >settime.out.$zone.1 2>&1
+cat template.db.in "${CSK}.key" >"$infile"
+private_type_record $zone $DEFAULT_ALGORITHM_NUMBER "$CSK" >>"$infile"
+cp $infile $zonefile
+$SIGNER -S -z -x -s now-1h -e now+30d -o $zone -O raw -f "${zonefile}.signed" $infile >signer.out.$zone.1 2>&1
+
index a055639d36a6732f7b92b2fbd2a67c32b2679eb9..05e1e9753ce7bbaff8e9e04da8f8d16c2172a841 100644 (file)
@@ -382,3 +382,134 @@ def test_rollover_multisigner(servers):
     isctest.kasp.check_apex(server, zone, ksks, zsks)
     isctest.kasp.check_subdomain(server, zone, ksks, zsks)
     isctest.kasp.check_dnssec_verify(server, zone)
+
+
+def check_rollover_step(server, zone, config, policy, keyprops, nextev):
+    ttl = int(config["dnskey-ttl"].total_seconds())
+    expected = isctest.kasp.policy_to_properties(ttl, keyprops)
+    isctest.kasp.check_zone_is_signed(server, zone)
+    keys = isctest.kasp.keydir_to_keylist(zone, server.identifier)
+    ksks = [k for k in keys if k.is_ksk()]
+    zsks = [k for k in keys if not k.is_ksk()]
+    isctest.kasp.check_keys(zone, keys, expected)
+
+    for kp in expected:
+        kp.set_expected_keytimes(config)
+
+        # Check that CDS publication/withdrawal is logged.
+        if "KSK" not in kp.metadata:
+            continue
+        if kp.metadata["KSK"] == "no":
+            continue
+        key = kp.key
+
+        if kp.metadata["DSState"] == "rumoured":
+            isctest.kasp.check_cdslog(server, zone, key, "CDS (SHA-256)")
+            isctest.kasp.check_cdslog(server, zone, key, "CDNSKEY")
+            isctest.kasp.check_cdslog_prohibit(server, zone, key, "CDS (SHA-384)")
+
+            # The DS can be introduced. We ignore any parent registration delay,
+            # so set the DS publish time to now.
+            server.rndc(f"dnssec -checkds -key {key.tag} published {zone}")
+
+        if kp.metadata["DSState"] == "unretentive":
+            # The DS can be withdrawn. We ignore any parent registration
+            # delay, so set the DS withdraw time to now.
+            server.rndc(f"dnssec -checkds -key {key.tag} withdrawn {zone}")
+
+    isctest.kasp.check_keytimes(keys, expected)
+    isctest.kasp.check_dnssecstatus(server, zone, keys, policy=policy)
+    isctest.kasp.check_apex(server, zone, ksks, zsks)
+    isctest.kasp.check_subdomain(server, zone, ksks, zsks)
+    isctest.kasp.check_dnssec_verify(server, zone)
+
+    def check_next_key_event():
+        return isctest.kasp.next_key_event_equals(server, zone, nextev)
+
+    isctest.run.retry_with_timeout(check_next_key_event, timeout=5)
+
+
+def test_rollover_enable_dnssec(servers):
+    server = servers["ns3"]
+    policy = "enable-dnssec"
+    config = {
+        "dnskey-ttl": timedelta(seconds=300),
+        "ds-ttl": timedelta(hours=2),
+        "max-zone-ttl": timedelta(hours=12),
+        "parent-propagation-delay": timedelta(hours=1),
+        "publish-safety": timedelta(minutes=5),
+        "retire-safety": timedelta(minutes=20),
+        "signatures-refresh": timedelta(days=7),
+        "signatures-validity": timedelta(days=14),
+        "zone-propagation-delay": timedelta(minutes=5),
+    }
+    alg = os.environ["DEFAULT_ALGORITHM_NUMBER"]
+    size = os.environ["DEFAULT_BITS"]
+
+    ipub = Ipub(config)
+    ipubC = IpubC(config, rollover=False)
+    iretZSK = Iret(config, rollover=False)
+    iretKSK = Iret(config, zsk=False, ksk=True, rollover=False)
+    offsets = {
+        "step1": 0,
+        "step2": -int(ipub.total_seconds()),
+        "step3": -int(iretZSK.total_seconds()),
+        "step4": -int(ipubC.total_seconds() + iretKSK.total_seconds()),
+    }
+
+    steps = [
+        {
+            # Step 1.
+            "zone": "step1.enable-dnssec.autosign",
+            "keyprops": [
+                f"csk unlimited {alg} {size} goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden offset:{offsets['step1']}",
+            ],
+            # Next key event is when the DNSKEY RRset becomes OMNIPRESENT,
+            # after the publication interval.
+            "nextev": ipub,
+        },
+        {
+            # Step 2.
+            "zone": "step2.enable-dnssec.autosign",
+            # The DNSKEY is omnipresent, but the zone signatures not yet.
+            # Thus, the DS remains hidden.
+            # dnskey: rumoured -> omnipresent
+            # krrsig: rumoured -> omnipresent
+            "keyprops": [
+                f"csk unlimited {alg} {size} goal:omnipresent dnskey:omnipresent krrsig:omnipresent zrrsig:rumoured ds:hidden offset:{offsets['step2']}",
+            ],
+            # Next key event is when the zone signatures become OMNIPRESENT,
+            # Minus the time already elapsed.
+            "nextev": iretZSK - ipub,
+        },
+        {
+            # Step 3.
+            "zone": "step3.enable-dnssec.autosign",
+            # All signatures should be omnipresent, so the DS can be submitted.
+            # zrrsig: rumoured -> omnipresent
+            # ds: hidden -> rumoured
+            "keyprops": [
+                f"csk unlimited {alg} {size} goal:omnipresent dnskey:omnipresent krrsig:omnipresent zrrsig:omnipresent ds:rumoured offset:{offsets['step3']}",
+            ],
+            # Next key event is when the DS can move to the OMNIPRESENT state.
+            # This is after the retire interval.
+            "nextev": iretKSK,
+        },
+        {
+            # Step 4.
+            "zone": "step4.enable-dnssec.autosign",
+            # DS has been published long enough.
+            # ds: rumoured -> omnipresent
+            "keyprops": [
+                f"csk unlimited {alg} {size} goal:omnipresent dnskey:omnipresent krrsig:omnipresent zrrsig:omnipresent ds:omnipresent offset:{offsets['step4']}",
+            ],
+            # Next key event is never, the zone dnssec-policy has been
+            # established. So we fall back to the default loadkeys interval.
+            "nextev": timedelta(hours=1),
+        },
+    ]
+
+    for step in steps:
+        check_rollover_step(
+            server, step["zone"], config, policy, step["keyprops"], step["nextev"]
+        )