]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Rewrite nsec3 system test to pytest (2/4)
authorMatthijs Mekking <matthijs@isc.org>
Tue, 30 Sep 2025 11:25:22 +0000 (13:25 +0200)
committerMatthijs Mekking <matthijs@isc.org>
Fri, 21 Nov 2025 12:50:13 +0000 (13:50 +0100)
This converts the nsec3 system test cases after to reconfiguring the
name server.

Two extra test for nsec3-change.kasp is updated. It depends on the
zone being updated, and a reconfig. This test code is moved to
tests_nsec3_reconfig.py.

Furthermore, an additional 'rndc signing -nsec3param' error test
case has been added.

bin/tests/system/nsec3/tests.sh
bin/tests/system/nsec3/tests_nsec3_initial.py
bin/tests/system/nsec3/tests_nsec3_reconfig.py [new file with mode: 0644]

index 0414fb15f9a6a0db8325740a4076c6cfb8037b0c..9a4a2d22f7d095b664d283852a715837fd66aaf7 100644 (file)
@@ -235,164 +235,6 @@ key_clear "KEY2"
 key_clear "KEY3"
 key_clear "KEY4"
 
-# Reconfig named.
-ret=0
-echo_i "reconfig dnssec-policy to trigger nsec3 rollovers"
-if [ $RSASHA1_SUPPORTED = 0 ]; then
-  copy_setports ns3/named2-fips.conf.in ns3/named.conf
-else
-  copy_setports ns3/named2-fips.conf.in ns3/named-fips.conf
-  # includes named-fips.conf
-  cp ns3/named2.conf.in ns3/named.conf
-fi
-rndc_reconfig ns3 10.53.0.3
-
-# Zone: nsec-to-nsec3.kasp. (reconfigured)
-set_zone_policy "nsec-to-nsec3.kasp" "nsec3" 1 3600
-set_nsec3param "0" "0"
-set_key_default_values "KEY1"
-echo_i "check zone ${ZONE} after reconfig"
-check_nsec3
-
-if [ $RSASHA1_SUPPORTED = 1 ]; then
-  # Zone: rsasha1-to-nsec3.kasp.
-  set_zone_policy "rsasha1-to-nsec3.kasp" "nsec3" 2 3600
-  set_server "ns3" "10.53.0.3"
-  set_key_rsasha1_values "KEY1"
-  set_key_states "KEY1" "hidden" "unretentive" "unretentive" "unretentive" "hidden"
-  set_keysigning "KEY1" "no"
-  set_zonesigning "KEY1" "no"
-  set_key_default_values "KEY2"
-  echo_i "check zone ${ZONE} after reconfig"
-  check_nsec3
-
-  # Zone: rsasha1-to-nsec3-wait.kasp.
-  set_zone_policy "rsasha1-to-nsec3-wait.kasp" "nsec3" 2 3600
-  set_server "ns3" "10.53.0.3"
-  set_key_rsasha1_values "KEY1"
-  set_key_states "KEY1" "hidden" "omnipresent" "omnipresent" "omnipresent" "omnipresent"
-  set_key_default_values "KEY2"
-  echo_i "check zone ${ZONE} after reconfig"
-  check_nsec
-
-  # Zone: nsec3-to-rsasha1.kasp.
-  set_zone_policy "nsec3-to-rsasha1.kasp" "rsasha1" 2 3600
-  set_nsec3param "1" "0"
-  set_server "ns3" "10.53.0.3"
-  set_key_default_values "KEY1"
-  set_key_states "KEY1" "hidden" "unretentive" "unretentive" "unretentive" "hidden"
-  set_keysigning "KEY1" "no"
-  set_zonesigning "KEY1" "no"
-  set_key_rsasha1_values "KEY2"
-  echo_i "check zone ${ZONE} after reconfig"
-  check_nsec
-
-  # Zone: nsec3-to-rsasha1-ds.kasp.
-  set_zone_policy "nsec3-to-rsasha1-ds.kasp" "rsasha1" 2 3600
-  set_nsec3param "1" "0"
-  set_server "ns3" "10.53.0.3"
-  set_key_default_values "KEY1"
-  set_key_states "KEY1" "hidden" "omnipresent" "omnipresent" "omnipresent" "omnipresent"
-  set_key_rsasha1_values "KEY2"
-  echo_i "check zone ${ZONE} after reconfig"
-  check_nsec
-
-  key_clear "KEY1"
-  key_clear "KEY2"
-fi
-
-# Zone: nsec3.kasp. (same)
-set_zone_policy "nsec3.kasp" "nsec3" 1 3600
-set_nsec3param "0" "0"
-set_key_default_values "KEY1"
-echo_i "check zone ${ZONE} after reconfig"
-check_nsec3
-
-# Zone: nsec3-dyamic.kasp. (same)
-set_zone_policy "nsec3-dynamic.kasp" "nsec3" 1 3600
-set_nsec3param "0" "0"
-set_key_default_values "KEY1"
-echo_i "check zone ${ZONE} after reconfig"
-check_nsec3
-
-# Zone: nsec3-change.kasp. (reconfigured)
-set_zone_policy "nsec3-change.kasp" "nsec3-other" 1 3600
-set_nsec3param "1" "8"
-set_key_default_values "KEY1"
-echo_i "check zone ${ZONE} after reconfig"
-check_nsec3
-
-# Test that NSEC3PARAM TTL is equal to new SOA MINIMUM.
-n=$((n + 1))
-echo_i "check TTL of NSEC3PARAM in zone $ZONE is updated after SOA MINIMUM changed ($n)"
-ret=0
-# Check NSEC3PARAM TTL.
-dig_with_opts +noquestion "@${SERVER}" "$ZONE" NSEC3PARAM >"dig.out.nsec3param.test$n" || ret=1
-grep "${ZONE}\..*900.*IN.*NSEC3PARAM" "dig.out.nsec3param.test$n" >/dev/null || ret=1
-test "$ret" -eq 0 || echo_i "failed"
-status=$((status + ret))
-
-# Using rndc signing -nsec3param (should fail)
-echo_i "use rndc signing -nsec3param ${ZONE} to change NSEC3 settings"
-rndccmd $SERVER signing -nsec3param 1 1 12 ffff $ZONE >rndc.signing.test$n.$ZONE || log_error "failed to call rndc signing -nsec3param $ZONE"
-grep "zone uses dnssec-policy, use rndc dnssec command instead" rndc.signing.test$n.$ZONE >/dev/null || log_error "rndc signing -nsec3param should fail"
-check_nsec3
-
-# Zone: nsec3-dynamic-change.kasp. (reconfigured)
-set_zone_policy "nsec3-dynamic-change.kasp" "nsec3-other" 1 3600
-set_nsec3param "1" "8"
-set_key_default_values "KEY1"
-echo_i "check zone ${ZONE} after reconfig"
-check_nsec3
-
-# Zone: nsec3-dynamic-to-inline.kasp. (same)
-set_zone_policy "nsec3-dynamic-to-inline.kasp" "nsec3" 1 3600
-set_nsec3param "0" "0"
-set_key_default_values "KEY1"
-echo_i "check zone ${ZONE} after reconfig"
-check_nsec3
-
-# Zone: nsec3-inline-to-dynamic.kasp. (same)
-set_zone_policy "nsec3-inline-to-dynamic.kasp" "nsec3" 1 3600
-set_nsec3param "0" "0"
-set_key_default_values "KEY1"
-echo_i "initial check zone ${ZONE}"
-check_nsec3
-
-# Zone: nsec3-to-nsec.kasp. (reconfigured)
-set_zone_policy "nsec3-to-nsec.kasp" "nsec" 1 3600
-set_nsec3param "1" "8"
-set_key_default_values "KEY1"
-echo_i "check zone ${ZONE} after reconfig"
-check_nsec
-
-# Zone: nsec3-to-optout.kasp. (reconfigured)
-# DISABLED:
-# There is a bug in the nsec3param building code that thinks when the
-# optout bit is changed, the chain already exists. [GL #2216]
-#set_zone_policy "nsec3-to-optout.kasp" "optout" 1 3600
-#set_nsec3param "1" "0"
-#set_key_default_values "KEY1"
-#echo_i "check zone ${ZONE} after reconfig"
-#check_nsec3
-
-# Zone: nsec3-from-optout.kasp. (reconfigured)
-# DISABLED:
-# There is a bug in the nsec3param building code that thinks when the
-# optout bit is changed, the chain already exists. [GL #2216]
-#set_zone_policy "nsec3-from-optout.kasp" "nsec3" 1 3600
-#set_nsec3param "0" "0"
-#set_key_default_values "KEY1"
-#echo_i "check zone ${ZONE} after reconfig"
-#check_nsec3
-
-# Zone: nsec3-other.kasp. (same)
-set_zone_policy "nsec3-other.kasp" "nsec3-other" 1 3600
-set_nsec3param "1" "8"
-set_key_default_values "KEY1"
-echo_i "check zone ${ZONE} after reconfig"
-check_nsec3
-
 # Test NSEC3 and NSEC3PARAM is the same after restart
 set_zone_policy "nsec3.kasp" "nsec3" 1 3600
 set_nsec3param "0" "0"
index 6f5cfcdafce4dd5ddc45a8cae2941fd60e0bfa23..99247f8973512027d9bc1affc70cc56fa772ec9e 100644 (file)
@@ -11,8 +11,6 @@
 
 # pylint: disable=redefined-outer-name,unused-import
 
-import shutil
-
 import dns.update
 import pytest
 
@@ -155,25 +153,6 @@ def test_nsec_case(ns3, params):
             )
 
 
-def wait_for_soa_update(server, fqdn):
-    verified = False
-    match = f"20 20 1814400 900"
-
-    for _ in range(5):
-        query = isctest.query.create(fqdn, dns.rdatatype.SOA)
-        response = isctest.query.tcp(query, server.ip, server.ports.dns, timeout=3)
-        for rrset in response.answer:
-            if match in rrset.to_text():
-                verified = True
-
-        if verified:
-            break
-
-        time.sleep(1)
-
-    return verified
-
-
 @pytest.mark.parametrize(
     "params",
     [
@@ -349,14 +328,3 @@ def test_nsec3_case(ns3, params):
     response = isctest.query.tcp(query, ns3.ip)
     assert response.rcode() == dns.rcode.NXDOMAIN
     check_auth_nsec3(response, iterations, optout, salt)
-
-    # Extra test for nsec3-change.kasp.
-    if zone == "nsec3-change.kasp":
-
-        shutil.copyfile(
-            f"{ns3.identifier}/template2.db.in", f"{ns3.identifier}/{zone}.db"
-        )
-        ns3.rndc(f"reload {zone}")
-
-        wait_for_soa_update(ns3, fqdn)
-        # After reconfig, the NSEC3PARAM TTL should match the new SOA MINIMUM.
diff --git a/bin/tests/system/nsec3/tests_nsec3_reconfig.py b/bin/tests/system/nsec3/tests_nsec3_reconfig.py
new file mode 100644 (file)
index 0000000..7ce5846
--- /dev/null
@@ -0,0 +1,355 @@
+# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+#
+# SPDX-License-Identifier: MPL-2.0
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0.  If a copy of the MPL was not distributed with this
+# file, you can obtain one at https://mozilla.org/MPL/2.0/.
+#
+# See the COPYRIGHT file distributed with this work for additional
+# information regarding copyright ownership.
+
+# pylint: disable=redefined-outer-name,unused-import
+
+import os
+import shutil
+import time
+
+import dns.update
+import pytest
+
+pytest.importorskip("dns", minversion="2.0.0")
+import isctest
+import isctest.mark
+from isctest.vars.algorithms import RSASHA1
+from nsec3.common import (
+    ALGORITHM,
+    SIZE,
+    default_config,
+    pytestmark,
+    check_auth_nsec,
+    check_auth_nsec3,
+    check_nsec3param,
+)
+
+
+@pytest.fixture(scope="module", autouse=True)
+def after_servers_start(ns3, templates):
+
+    def wait_for_soa_update():
+        match = "20 20 1814400 900"
+
+        for _ in range(5):
+            query = isctest.query.create(fqdn, dns.rdatatype.SOA)
+            response = isctest.query.tcp(query, ns3.ip)
+            rrset = response.get_rrset(
+                response.answer,
+                dns.name.from_text(fqdn),
+                dns.rdataclass.IN,
+                dns.rdatatype.SOA,
+            )
+            if match in str(rrset[0]):
+                return True
+
+        return False
+
+    # Extra test for nsec3-change.kasp.
+    zone = "nsec3-change.kasp"
+    nsdir = ns3.identifier
+    fqdn = f"{zone}."
+    isctest.kasp.wait_keymgr_done(ns3, zone)
+    shutil.copyfile(f"{nsdir}/template2.db.in", f"{nsdir}/{zone}.db")
+    ns3.rndc(f"reload {zone}")
+
+    isctest.run.retry_with_timeout(wait_for_soa_update, timeout=5)
+    # After reconfig, the NSEC3PARAM TTL should match the new SOA MINIMUM.
+
+    # Ensure rsasha1-to-nsec3-wait.kasp is fully signed prior to reconfig.
+    with_rsasha1 = "RSASHA1_SUPPORTED"
+    assert with_rsasha1 in os.environ, f"{with_rsasha1} env variable undefined"
+    if os.getenv(with_rsasha1) == "1":
+        zone = "rsasha1-to-nsec3-wait.kasp"
+        isctest.kasp.check_dnssec_verify(ns3, zone)
+
+    # Reconfigure.
+    templates.render(f"{nsdir}/named-fips.conf", {"reconfiged": True})
+    templates.render(f"{nsdir}/named-rsasha1.conf", {"reconfiged": True})
+    ns3.reconfigure()
+
+
+@pytest.mark.parametrize(
+    "params",
+    [
+        pytest.param(
+            {
+                "zone": "rsasha1-to-nsec3-wait.kasp",
+                "policy": "nsec3",
+                "key-properties": [
+                    f"csk 0 {RSASHA1.number} 2048 goal:hidden dnskey:omnipresent krrsig:omnipresent zrrsig:omnipresent ds:omnipresent",
+                    f"csk 0 {ALGORITHM} {SIZE} goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden",
+                ],
+            },
+            id="rsasha1-to-nsec3-wait.kasp",
+            marks=isctest.mark.with_algorithm("RSASHA1"),
+        ),
+        pytest.param(
+            {
+                "zone": "nsec3-to-rsasha1.kasp",
+                "policy": "rsasha1",
+                "key-properties": [
+                    f"csk 0 {ALGORITHM} {SIZE} goal:hidden dnskey:unretentive krrsig:unretentive zrrsig:unretentive ds:hidden",
+                    f"csk 0 {RSASHA1.number} 2048 goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden",
+                ],
+            },
+            id="nsec3-to-rsasha1.kasp",
+            marks=isctest.mark.with_algorithm("RSASHA1"),
+        ),
+        pytest.param(
+            {
+                "zone": "nsec3-to-rsasha1-ds.kasp",
+                "policy": "rsasha1",
+                "key-properties": [
+                    f"csk 0 {ALGORITHM} {SIZE} goal:hidden dnskey:omnipresent krrsig:omnipresent zrrsig:omnipresent ds:omnipresent",
+                    f"csk 0 {RSASHA1.number} 2048 goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden",
+                ],
+            },
+            id="nsec3-to-rsasha1-ds.kasp",
+            marks=isctest.mark.with_algorithm("RSASHA1"),
+        ),
+        pytest.param(
+            {
+                "zone": "nsec3-to-nsec.kasp",
+                "policy": "nsec",
+                "key-properties": [
+                    f"csk 0 {ALGORITHM} {SIZE} goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden",
+                ],
+            },
+            id="nsec3-to-nsec.kasp",
+        ),
+    ],
+)
+def test_nsec_case(ns3, params):
+    # Get test parameters.
+    zone = params["zone"]
+    fqdn = f"{zone}."
+    policy = params["policy"]
+    keydir = ns3.identifier
+    config = default_config
+    ttl = int(config["dnskey-ttl"].total_seconds())
+    expected = isctest.kasp.policy_to_properties(ttl=ttl, keys=params["key-properties"])
+
+    # Test case.
+    isctest.log.info(f"check nsec case zone {zone} policy {policy}")
+
+    # First make sure the zone is properly signed.
+    isctest.kasp.wait_keymgr_done(ns3, zone, reconfig=True)
+
+    # Key files.
+    keys = isctest.kasp.keydir_to_keylist(zone, keydir)
+
+    isctest.kasp.check_keys(zone, keys, expected)
+    isctest.kasp.check_dnssec_verify(ns3, zone)
+    isctest.kasp.check_apex(ns3, zone, keys, [])
+
+    query = isctest.query.create(fqdn, dns.rdatatype.NSEC3PARAM)
+    response = isctest.query.tcp(query, ns3.ip)
+    assert response.rcode() == dns.rcode.NOERROR
+    assert len(response.answer) == 0
+    check_auth_nsec(response)
+
+    query = isctest.query.create(f"nosuchname.{fqdn}", dns.rdatatype.A)
+    response = isctest.query.tcp(query, ns3.ip)
+    assert response.rcode() == dns.rcode.NXDOMAIN
+    check_auth_nsec(response)
+
+
+@pytest.mark.parametrize(
+    "params",
+    [
+        pytest.param(
+            {
+                "zone": "nsec-to-nsec3.kasp",
+                "policy": "nsec3",
+                "key-properties": [
+                    f"csk 0 {ALGORITHM} {SIZE} goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden",
+                ],
+            },
+            id="nsec-to-nsec3.kasp",
+        ),
+        pytest.param(
+            {
+                "zone": "rsasha1-to-nsec3.kasp",
+                "policy": "nsec3",
+                "key-properties": [
+                    f"csk 0 {RSASHA1.number} 2048 goal:hidden dnskey:unretentive krrsig:unretentive zrrsig:unretentive ds:hidden",
+                    f"csk 0 {ALGORITHM} {SIZE} goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden",
+                ],
+            },
+            id="rsasha1-to-nsec3.kasp",
+            marks=isctest.mark.with_algorithm("RSASHA1"),
+        ),
+        pytest.param(
+            {
+                "zone": "nsec3.kasp",
+                "policy": "nsec3",
+                "key-properties": [
+                    f"csk 0 {ALGORITHM} {SIZE} goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden",
+                ],
+            },
+            id="nsec3.kasp",
+        ),
+        pytest.param(
+            {
+                "zone": "nsec3-dynamic.kasp",
+                "policy": "nsec3",
+                "key-properties": [
+                    f"csk 0 {ALGORITHM} {SIZE} goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden",
+                ],
+            },
+            id="nsec3-dynamic.kasp",
+        ),
+        pytest.param(
+            {
+                "zone": "nsec3-change.kasp",
+                "policy": "nsec3",
+                "soa-minimum": 900,
+                "nsec3param": {
+                    "optout": 1,
+                    "salt-length": 8,
+                },
+                "key-properties": [
+                    f"csk 0 {ALGORITHM} {SIZE} goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden",
+                ],
+            },
+            id="nsec3-change.kasp",
+        ),
+        pytest.param(
+            {
+                "zone": "nsec3-dynamic-change.kasp",
+                "policy": "nsec3-other",
+                "nsec3param": {
+                    "optout": 1,
+                    "salt-length": 8,
+                },
+                "key-properties": [
+                    f"csk 0 {ALGORITHM} {SIZE} goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden",
+                ],
+            },
+            id="nsec3-dynamic-change.kasp",
+        ),
+        pytest.param(
+            {
+                "zone": "nsec3-dynamic-to-inline.kasp",
+                "policy": "nsec3",
+                "key-properties": [
+                    f"csk 0 {ALGORITHM} {SIZE} goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden",
+                ],
+            },
+            id="nsec3-dynamic-to-inline.kasp",
+        ),
+        pytest.param(
+            {
+                "zone": "nsec3-inline-to-dynamic.kasp",
+                "policy": "nsec3",
+                "key-properties": [
+                    f"csk 0 {ALGORITHM} {SIZE} goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden",
+                ],
+            },
+            id="nsec3-inline-to-dynamic.kasp",
+        ),
+        # DISABLED:
+        # There is a bug in the nsec3param building code that thinks when the
+        # optout bit is changed, the chain already exists. [GL #2216]
+        # pytest.param(
+        #    {
+        #        "zone": "nsec3-to-optout.kasp",
+        #        "policy": "nsec3",
+        #        "nsec3param": {
+        #            "optout": 1,
+        #            "salt-length": 0,
+        #        },
+        #        "key-properties": [
+        #            f"csk 0 {ALGORITHM} {SIZE} goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden",
+        #        ],
+        #    },
+        #    id="nsec3-to-optout.kasp",
+        # ),
+        # DISABLED:
+        # There is a bug in the nsec3param building code that thinks when the
+        # optout bit is changed, the chain already exists. [GL #2216]
+        # pytest.param(
+        #    {
+        #        "zone": "nsec3-from-optout.kasp",
+        #        "policy": "optout",
+        #        "key-properties": [
+        #            f"csk 0 {ALGORITHM} {SIZE} goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden",
+        #        ],
+        #    },
+        #    id="nsec3-from-optout.kasp",
+        # ),
+        pytest.param(
+            {
+                "zone": "nsec3-other.kasp",
+                "policy": "nsec3-other",
+                "nsec3param": {
+                    "optout": 1,
+                    "salt-length": 8,
+                },
+                "key-properties": [
+                    f"csk 0 {ALGORITHM} {SIZE} goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden",
+                ],
+            },
+            id="nsec3-other.kasp",
+        ),
+    ],
+)
+def test_nsec3_case(ns3, params):
+    # Get test parameters.
+    zone = params["zone"]
+    fqdn = f"{zone}."
+    policy = params["policy"]
+    keydir = ns3.identifier
+    config = default_config
+    ttl = int(config.get("dnskey-ttl", 3600).total_seconds())
+    minimum = params.get("soa-minimum", 3600)
+    expected = isctest.kasp.policy_to_properties(ttl=ttl, keys=params["key-properties"])
+
+    iterations = 0
+    optout = 0
+    saltlen = 0
+    if "nsec3param" in params:
+        optout = params["nsec3param"].get("optout", 0)
+        saltlen = params["nsec3param"].get("salt-length", 0)
+
+    match = f"{fqdn} {minimum} IN NSEC3PARAM 1 0 {iterations}"
+
+    # Test case.
+    isctest.log.info(f"check nsec3 case zone {zone} policy {policy}")
+
+    # First make sure the zone is properly signed.
+    isctest.kasp.wait_keymgr_done(ns3, zone, reconfig=True)
+
+    keys = isctest.kasp.keydir_to_keylist(zone, keydir)
+    isctest.kasp.check_keys(zone, keys, expected)
+    isctest.kasp.check_dnssec_verify(ns3, zone)
+    isctest.kasp.check_apex(ns3, zone, keys, [])
+
+    query = isctest.query.create(fqdn, dns.rdatatype.NSEC3PARAM)
+    response = isctest.query.tcp(query, ns3.ip)
+    assert response.rcode() == dns.rcode.NOERROR
+
+    salt = check_nsec3param(response, match, saltlen)
+
+    query = isctest.query.create(f"nosuchname.{fqdn}", dns.rdatatype.A)
+    response = isctest.query.tcp(query, ns3.ip)
+    assert response.rcode() == dns.rcode.NXDOMAIN
+    check_auth_nsec3(response, iterations, optout, salt)
+
+    # Extra test for nsec3-change.kasp.
+    if zone == "nsec3-change.kasp":
+        # Using rndc signing -nsec3param (should fail)
+        isctest.log.info(
+            f"use rndc signing -nsec3param {zone} to change NSEC3 settings"
+        )
+        response = ns3.rndc(f"signing -nsec3param 1 1 12 ffff {zone}")
+        assert "zone uses dnssec-policy, use rndc dnssec command instead" in response