From e81cc1520a826a362b2fa31772733eb932180576 Mon Sep 17 00:00:00 2001 From: Matthijs Mekking Date: Tue, 30 Sep 2025 09:13:38 +0200 Subject: [PATCH 01/13] Rewrite nsec3 system test to pytest (1/4) This converts all the nsec3 system test cases prior to reconfiguring the name server. There are two main classes, one that tests the zone is correctly signed with NSEC, the other with NSEC3. Two extra tests for nsec3-dynamic-update-inline.kasp and nsec3-change.kasp are also rewritten. For the former, we need to change the 'nsupdate' definition to be able to set the expected RCODE. --- bin/tests/system/isctest/instance.py | 10 +- bin/tests/system/nsec3/tests.sh | 153 ------ bin/tests/system/nsec3/tests_nsec3_initial.py | 443 ++++++++++++++++++ 3 files changed, 450 insertions(+), 156 deletions(-) create mode 100644 bin/tests/system/nsec3/tests_nsec3_initial.py diff --git a/bin/tests/system/isctest/instance.py b/bin/tests/system/isctest/instance.py index 268dcf09d0..d6400c5e39 100644 --- a/bin/tests/system/isctest/instance.py +++ b/bin/tests/system/isctest/instance.py @@ -154,7 +154,9 @@ class NamedInstance: return response - def nsupdate(self, update_msg: dns.message.Message): + def nsupdate( + self, update_msg: dns.message.Message, expected_rcode=dns.rcode.NOERROR + ): """ Issue a dynamic update to a server's zone. """ @@ -168,12 +170,14 @@ class NamedInstance: self.ip, self.ports.dns, timeout=3, - expected_rcode=dns.rcode.NOERROR, + expected_rcode=expected_rcode, ) except dns.exception.Timeout as exc: msg = f"update timeout for {zone}" raise dns.exception.Timeout(msg) from exc - debug(f"update of zone {zone} to server {self.ip} successful") + debug( + f"update of zone {zone} to server {self.ip} finished with {expected_rcode}" + ) return response def watch_log_from_start( diff --git a/bin/tests/system/nsec3/tests.sh b/bin/tests/system/nsec3/tests.sh index bcb1144ada..0414fb15f9 100644 --- a/bin/tests/system/nsec3/tests.sh +++ b/bin/tests/system/nsec3/tests.sh @@ -235,159 +235,6 @@ key_clear "KEY2" key_clear "KEY3" key_clear "KEY4" -# Zone: nsec-to-nsec3.kasp. -set_zone_policy "nsec-to-nsec3.kasp" "nsec" 1 3600 -set_server "ns3" "10.53.0.3" -set_key_default_values "KEY1" -echo_i "initial check zone ${ZONE}" -check_nsec - -if [ $RSASHA1_SUPPORTED = 1 ]; then - # Zone: rsasha1-to-nsec3.kasp. - set_zone_policy "rsasha1-to-nsec3.kasp" "rsasha1" 1 3600 - set_server "ns3" "10.53.0.3" - set_key_rsasha1_values "KEY1" - echo_i "initial check zone ${ZONE}" - check_nsec - - # Zone: rsasha1-to-nsec3-wait.kasp. - set_zone_policy "rsasha1-to-nsec3-wait.kasp" "rsasha1" 1 3600 - set_server "ns3" "10.53.0.3" - set_key_rsasha1_values "KEY1" - set_key_states "KEY1" "omnipresent" "omnipresent" "omnipresent" "omnipresent" "omnipresent" - echo_i "initial check zone ${ZONE}" - check_nsec - - # Zone: nsec3-to-rsasha1.kasp. - set_zone_policy "nsec3-to-rsasha1.kasp" "nsec3" 1 3600 - set_server "ns3" "10.53.0.3" - set_key_rsasha1_values "KEY1" - echo_i "initial check zone ${ZONE}" - check_nsec3 - - # Zone: nsec3-to-rsasha1-ds.kasp. - set_zone_policy "nsec3-to-rsasha1-ds.kasp" "nsec3" 1 3600 - set_server "ns3" "10.53.0.3" - set_key_rsasha1_values "KEY1" - set_key_states "KEY1" "omnipresent" "omnipresent" "omnipresent" "omnipresent" "omnipresent" - echo_i "initial check zone ${ZONE}" - check_nsec3 -fi - -# Zone: nsec3.kasp. -set_zone_policy "nsec3.kasp" "nsec3" 1 3600 -set_nsec3param "0" "0" -set_key_default_values "KEY1" -echo_i "initial check zone ${ZONE}" -check_nsec3 - -# Zone: nsec3-dynamic.kasp. -set_zone_policy "nsec3-dynamic.kasp" "nsec3" 1 3600 -set_nsec3param "0" "0" -set_key_default_values "KEY1" -echo_i "initial check zone ${ZONE}" -check_nsec3 - -# Zone: nsec3-change.kasp. -set_zone_policy "nsec3-change.kasp" "nsec3" 1 3600 -set_nsec3param "0" "0" -set_key_default_values "KEY1" -echo_i "initial check zone ${ZONE}" -check_nsec3 - -# Test that NSEC3PARAM TTL is equal to SOA MINIMUM. -n=$((n + 1)) -echo_i "check TTL of NSEC3PARAM in zone $ZONE is equal to SOA MINIMUM ($n)" -ret=0 -dig_with_opts +noquestion "@${SERVER}" "$ZONE" NSEC3PARAM >"dig.out.test$n" || ret=1 -grep "${ZONE}\..*3600.*IN.*NSEC3PARAM" "dig.out.test$n" >/dev/null || ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -# Update SOA MINIMUM. -cp "${DIR}/template2.db.in" "${DIR}/${ZONE}.db" -rndccmd $SERVER reload $ZONE >rndc.reload.test$n.$ZONE || log_error "failed to call rndc reload $ZONE" -_wait_for_new_soa() { - dig_with_opts +noquestion "@${SERVER}" "$ZONE" SOA >"dig.out.soa.test$n" || return 1 - grep "${ZONE}\..*IN.*SOA.*mname1..*..*20.*20.*.1814400.*900" "dig.out.soa.test$n" >/dev/null || return 1 -} -retry_quiet 10 _wait_for_new_soa || log_error "failed to update SOA record in zone $ZONE" - -# Zone: nsec3-dynamic-change.kasp. -set_zone_policy "nsec3-dynamic-change.kasp" "nsec3" 1 3600 -set_nsec3param "0" "0" -set_key_default_values "KEY1" -echo_i "initial check zone ${ZONE}" -check_nsec3 - -# Zone: nsec3-dynamic-to-inline.kasp. -set_zone_policy "nsec3-dynamic-to-inline.kasp" "nsec3" 1 3600 -set_nsec3param "0" "0" -set_key_default_values "KEY1" -echo_i "initial check zone ${ZONE}" -check_nsec3 - -# Zone: nsec3-inline-to-dynamic.kasp. -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. -set_zone_policy "nsec3-to-nsec.kasp" "nsec3" 1 3600 -set_nsec3param "0" "0" -set_key_default_values "KEY1" -echo_i "initial check zone ${ZONE}" -check_nsec3 - -# Zone: nsec3-to-optout.kasp. -set_zone_policy "nsec3-to-optout.kasp" "nsec3" 1 3600 -set_nsec3param "0" "0" -set_key_default_values "KEY1" -echo_i "initial check zone ${ZONE}" -check_nsec3 - -# Zone: nsec3-from-optout.kasp. -set_zone_policy "nsec3-from-optout.kasp" "optout" 1 3600 -set_nsec3param "1" "0" -set_key_default_values "KEY1" -echo_i "initial check zone ${ZONE}" -check_nsec3 - -# Zone: nsec3-other.kasp. -set_zone_policy "nsec3-other.kasp" "nsec3-other" 1 3600 -set_nsec3param "1" "8" -set_key_default_values "KEY1" -echo_i "initial check zone ${ZONE}" -check_nsec3 - -# Zone: nsec3-xfr-inline.kasp. -# This is a secondary zone, where the primary is signed with NSEC3 but -# the dnssec-policy dictates NSEC. -set_zone_policy "nsec3-xfr-inline.kasp" "nsec" 1 3600 -set_key_default_values "KEY1" -echo_i "initial check zone ${ZONE}" -check_nsec - -# Zone: nsec3-dynamic-update-inline.kasp. -set_zone_policy "nsec3-dynamic-update-inline.kasp" "nsec" 1 3600 -set_key_default_values "KEY1" -echo_i "initial check zone ${ZONE}" -check_nsec - -n=$((n + 1)) -echo_i "dynamic update dnssec-policy zone ${ZONE} with NSEC3 ($n)" -ret=0 -$NSUPDATE >update.out.$ZONE.test$n 2>&1 < Date: Tue, 30 Sep 2025 11:34:13 +0200 Subject: [PATCH 02/13] Convert "in"-style templates to jinja Change the named.conf templating to make use of jinja template rendering. The ns2 server is trivial. The ns3 server configuration structure has changed: The common configuration is moved out of named-fips.conf. The main named.conf file is in named.conf.j2. It always includes the common part, named-common.conf.j2, and the FIPS part, named-fips.conf.j2. The named-fips.conf.j2 and named-rsasha1.conf.j2 templates are rendered differently depending on the reconfiged status. Mainly the dnssec-policy for zones are different after reconfiguration, but there are some other changes to, for example some zones change their inline-signing setting. Some zones only exist prior or after the configuration. Finally, this is a bit hackish: If RSASHA1 is supported, named.conf includes "named-rsasha1.conf", otherwise it includes the deliberately empty "named-rsasha0.conf". --- .../ns2/{named.conf.in => named.conf.j2} | 0 .../system/nsec3/ns3/named-common.conf.j2 | 52 ++++++ ...{named-fips.conf.in => named-fips.conf.j2} | 109 ++++++------ .../{named1.conf.in => named-rsasha1.conf.j2} | 14 +- bin/tests/system/nsec3/ns3/named.conf.j2 | 21 +++ .../system/nsec3/ns3/named2-fips.conf.in | 157 ------------------ bin/tests/system/nsec3/ns3/named2.conf.in | 71 -------- bin/tests/system/nsec3/setup.sh | 8 - bin/tests/system/nsec3/tests_nsec3_initial.py | 3 + 9 files changed, 136 insertions(+), 299 deletions(-) rename bin/tests/system/nsec3/ns2/{named.conf.in => named.conf.j2} (100%) create mode 100644 bin/tests/system/nsec3/ns3/named-common.conf.j2 rename bin/tests/system/nsec3/ns3/{named-fips.conf.in => named-fips.conf.j2} (64%) rename bin/tests/system/nsec3/ns3/{named1.conf.in => named-rsasha1.conf.j2} (83%) create mode 100644 bin/tests/system/nsec3/ns3/named.conf.j2 delete mode 100644 bin/tests/system/nsec3/ns3/named2-fips.conf.in delete mode 100644 bin/tests/system/nsec3/ns3/named2.conf.in diff --git a/bin/tests/system/nsec3/ns2/named.conf.in b/bin/tests/system/nsec3/ns2/named.conf.j2 similarity index 100% rename from bin/tests/system/nsec3/ns2/named.conf.in rename to bin/tests/system/nsec3/ns2/named.conf.j2 diff --git a/bin/tests/system/nsec3/ns3/named-common.conf.j2 b/bin/tests/system/nsec3/ns3/named-common.conf.j2 new file mode 100644 index 0000000000..5dc8a7a552 --- /dev/null +++ b/bin/tests/system/nsec3/ns3/named-common.conf.j2 @@ -0,0 +1,52 @@ +/* + * 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. + */ + +options { + query-source address 10.53.0.3; + notify-source 10.53.0.3; + transfer-source 10.53.0.3; + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.3; }; + listen-on-v6 { none; }; + allow-transfer { any; }; + recursion no; + dnssec-validation no; +}; + +key rndc_key { + secret "1234abcd8765"; + algorithm @DEFAULT_HMAC@; +}; + +controls { + inet 10.53.0.3 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; +}; + +dnssec-policy "nsec" { + // no need to change configuration: if no 'nsec3param' is set, + // NSEC will be used; +}; + +dnssec-policy "nsec3" { + nsec3param; +}; + +dnssec-policy "optout" { + nsec3param optout yes; +}; + +dnssec-policy "nsec3-other" { + nsec3param iterations 0 optout yes salt-length 8; +}; + diff --git a/bin/tests/system/nsec3/ns3/named-fips.conf.in b/bin/tests/system/nsec3/ns3/named-fips.conf.j2 similarity index 64% rename from bin/tests/system/nsec3/ns3/named-fips.conf.in rename to bin/tests/system/nsec3/ns3/named-fips.conf.j2 index 4ed7cc0427..5029457598 100644 --- a/bin/tests/system/nsec3/ns3/named-fips.conf.in +++ b/bin/tests/system/nsec3/ns3/named-fips.conf.j2 @@ -11,52 +11,18 @@ * information regarding copyright ownership. */ -// NS3 - -dnssec-policy "nsec" { - // no need to change configuration: if no 'nsec3param' is set, - // NSEC will be used; -}; - -dnssec-policy "nsec3" { - nsec3param; -}; - -dnssec-policy "optout" { - nsec3param optout yes; -}; - -dnssec-policy "nsec3-other" { - nsec3param iterations 0 optout yes salt-length 8; -}; - -options { - query-source address 10.53.0.3; - notify-source 10.53.0.3; - transfer-source 10.53.0.3; - port @PORT@; - pid-file "named.pid"; - listen-on { 10.53.0.3; }; - listen-on-v6 { none; }; - allow-transfer { any; }; - recursion no; - dnssec-validation no; -}; - -key rndc_key { - secret "1234abcd8765"; - algorithm @DEFAULT_HMAC@; -}; - -controls { - inet 10.53.0.3 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; -}; +{% set reconfiged = reconfiged | default(False) %} +{% set nsec_to_nsec3 = "nsec" if not reconfiged else "nsec3" %} +{% set nsec3_to_nsec = "nsec3" if not reconfiged else "nsec" %} +{% set nsec3_change = "nsec3" if not reconfiged else "nsec3-other" %} +{% set nsec3_from_optout = "optout" if not reconfiged else "nsec3" %} +{% set nsec3_to_optout = "nsec3" if not reconfiged else "optout" %} /* This zone starts with NSEC, but will be reconfigured to use NSEC3. */ zone "nsec-to-nsec3.kasp" { type primary; file "nsec-to-nsec3.kasp.db"; - dnssec-policy "nsec"; + dnssec-policy "@nsec_to_nsec3@"; }; /* These zones use the default NSEC3 settings. */ @@ -84,14 +50,14 @@ zone "nsec3-other.kasp" { zone "nsec3-change.kasp" { type primary; file "nsec3-change.kasp.db"; - dnssec-policy "nsec3"; + dnssec-policy "@nsec3_change@"; }; zone "nsec3-dynamic-change.kasp" { type primary; file "nsec3-dynamic-change.kasp.db"; inline-signing no; - dnssec-policy "nsec3"; + dnssec-policy "@nsec3_change@"; allow-update { any; }; }; @@ -99,24 +65,27 @@ zone "nsec3-dynamic-change.kasp" { zone "nsec3-to-optout.kasp" { type primary; file "nsec3-to-optout.kasp.db"; - dnssec-policy "nsec3"; + dnssec-policy "@nsec3_to_optout@"; }; /* The zone will be reconfigured to disable opt-out. */ zone "nsec3-from-optout.kasp" { type primary; file "nsec3-from-optout.kasp.db"; - dnssec-policy "optout"; + dnssec-policy "@nsec3_from_optout@"; }; /* The zone starts with NSEC3, but will be reconfigured to use NSEC. */ zone "nsec3-to-nsec.kasp" { type primary; file "nsec3-to-nsec.kasp.db"; - dnssec-policy "nsec3"; + dnssec-policy "@nsec3_to_nsec@"; }; -/* The zone fails to load, this should not prevent shutdown. */ +/* + * The zone fails to load, this should not prevent shutdown. + * The zone is fixed after a reconfig. + */ zone "nsec3-fails-to-load.kasp" { type primary; file "nsec3-fails-to-load.kasp.db"; @@ -128,28 +97,56 @@ zone "nsec3-fails-to-load.kasp" { zone "nsec3-dynamic-to-inline.kasp" { type primary; file "nsec3-dynamic-to-inline.kasp.db"; - inline-signing no; dnssec-policy "nsec3"; +{% if not reconfiged %} allow-update { any; }; + inline-signing no; +{% endif %} }; zone "nsec3-inline-to-dynamic.kasp" { - type primary; - file "nsec3-inline-to-dynamic.kasp.db"; - dnssec-policy "nsec3"; + type primary; + file "nsec3-inline-to-dynamic.kasp.db"; + dnssec-policy "nsec3"; +{% if reconfiged %} + allow-update { any; }; + inline-signing no; +{% endif %} + }; -/* Test adding a NSEC3 record to an inline-signing dnssec-policy zone. */ +{% if not reconfiged %} + +/* + * Test adding a NSEC3 record to an inline-signing dnssec-policy zone. + */ zone "nsec3-dynamic-update-inline.kasp" { - type primary; - file "nsec3-dynamic-update-inline.kasp.db"; - allow-update { any; }; - dnssec-policy "nsec"; + type primary; + file "nsec3-dynamic-update-inline.kasp.db"; + allow-update { any; }; + dnssec-policy "nsec"; }; +/* + * This zone will have an empty nonterminal node added and a node deleted. + */ zone "nsec3-xfr-inline.kasp" { type secondary; file "nsec3-xfr-inline.kasp.db"; dnssec-policy "nsec"; primaries { 10.53.0.2; }; }; + +{% else %} + +/* + * This zone will have an empty nonterminal node added and a node deleted. + */ +zone "nsec3-ent.kasp" { + type primary; + file "nsec3-ent.kasp.db"; + dnssec-policy "nsec3"; + inline-signing yes; +}; + +{% endif %} diff --git a/bin/tests/system/nsec3/ns3/named1.conf.in b/bin/tests/system/nsec3/ns3/named-rsasha1.conf.j2 similarity index 83% rename from bin/tests/system/nsec3/ns3/named1.conf.in rename to bin/tests/system/nsec3/ns3/named-rsasha1.conf.j2 index 9b1235e36f..c2cbf485ce 100644 --- a/bin/tests/system/nsec3/ns3/named1.conf.in +++ b/bin/tests/system/nsec3/ns3/named-rsasha1.conf.j2 @@ -11,9 +11,9 @@ * information regarding copyright ownership. */ -// NS3 - -include "named-fips.conf"; +{% set reconfiged = reconfiged | default(False) %} +{% set rsasha1_to_nsec3 = "rsasha1" if not reconfiged else "nsec3" %} +{% set nsec3_to_rsasha1 = "nsec3" if not reconfiged else "rsasha1" %} dnssec-policy "rsasha1" { keys { @@ -29,7 +29,7 @@ dnssec-policy "rsasha1" { zone "rsasha1-to-nsec3.kasp" { type primary; file "rsasha1-to-nsec3.kasp.db"; - dnssec-policy "rsasha1"; + dnssec-policy "@rsasha1_to_nsec3@"; }; /* @@ -40,7 +40,7 @@ zone "rsasha1-to-nsec3.kasp" { zone "rsasha1-to-nsec3-wait.kasp" { type primary; file "rsasha1-to-nsec3-wait.kasp.db"; - dnssec-policy "rsasha1"; + dnssec-policy "@rsasha1_to_nsec3@"; }; /* @@ -51,7 +51,7 @@ zone "rsasha1-to-nsec3-wait.kasp" { zone "nsec3-to-rsasha1.kasp" { type primary; file "nsec3-to-rsasha1.kasp.db"; - dnssec-policy "nsec3"; + dnssec-policy "@nsec3_to_rsasha1@"; }; /* @@ -63,5 +63,5 @@ zone "nsec3-to-rsasha1.kasp" { zone "nsec3-to-rsasha1-ds.kasp" { type primary; file "nsec3-to-rsasha1-ds.kasp.db"; - dnssec-policy "nsec3"; + dnssec-policy "@nsec3_to_rsasha1@"; }; diff --git a/bin/tests/system/nsec3/ns3/named.conf.j2 b/bin/tests/system/nsec3/ns3/named.conf.j2 new file mode 100644 index 0000000000..7dd06ad83c --- /dev/null +++ b/bin/tests/system/nsec3/ns3/named.conf.j2 @@ -0,0 +1,21 @@ +/* + * 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. + */ + +// NS3 + +include "named-common.conf"; +include "named-fips.conf"; + +{% if RSASHA1_SUPPORTED == "1" %} +include "named-rsasha1.conf"; +{% endif %} diff --git a/bin/tests/system/nsec3/ns3/named2-fips.conf.in b/bin/tests/system/nsec3/ns3/named2-fips.conf.in deleted file mode 100644 index 2c9a2b7e20..0000000000 --- a/bin/tests/system/nsec3/ns3/named2-fips.conf.in +++ /dev/null @@ -1,157 +0,0 @@ -/* - * 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. - */ - -// NS3 - -dnssec-policy "nsec" { - // no need to change configuration: if no 'nsec3param' is set, - // NSEC will be used; -}; - -dnssec-policy "nsec3" { - nsec3param; -}; - -dnssec-policy "optout" { - nsec3param optout yes; -}; - -dnssec-policy "nsec3-other" { - nsec3param iterations 0 optout yes salt-length 8; -}; - -options { - query-source address 10.53.0.3; - notify-source 10.53.0.3; - transfer-source 10.53.0.3; - port @PORT@; - pid-file "named.pid"; - listen-on { 10.53.0.3; }; - listen-on-v6 { none; }; - allow-transfer { any; }; - recursion no; - dnssec-validation no; -}; - -key rndc_key { - secret "1234abcd8765"; - algorithm @DEFAULT_HMAC@; -}; - -controls { - inet 10.53.0.3 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; -}; - -/* This zone starts with NSEC, but will be reconfigured to use NSEC3. */ -zone "nsec-to-nsec3.kasp" { - type primary; - file "nsec-to-nsec3.kasp.db"; - //dnssec-policy "nsec"; - dnssec-policy "nsec3"; -}; - -/* These zones use the default NSEC3 settings. */ -zone "nsec3.kasp" { - type primary; - file "nsec3.kasp.db"; - dnssec-policy "nsec3"; -}; - -zone "nsec3-dynamic.kasp" { - type primary; - file "nsec3-dynamic.kasp.db"; - dnssec-policy "nsec3"; - allow-update { any; }; -}; - -/* This zone uses non-default NSEC3 settings. */ -zone "nsec3-other.kasp" { - type primary; - file "nsec3-other.kasp.db"; - dnssec-policy "nsec3-other"; -}; - -/* These zone will be reconfigured to use other NSEC3 settings. */ -zone "nsec3-change.kasp" { - type primary; - file "nsec3-change.kasp.db"; - //dnssec-policy "nsec3"; - dnssec-policy "nsec3-other"; -}; - -zone "nsec3-dynamic-change.kasp" { - type primary; - file "nsec3-dynamic-change.kasp.db"; - //dnssec-policy "nsec3"; - inline-signing no; - dnssec-policy "nsec3-other"; - allow-update { any; }; -}; - -/* The zone will be reconfigured to use opt-out. */ -zone "nsec3-to-optout.kasp" { - type primary; - file "nsec3-to-optout.kasp.db"; - //dnssec-policy "nsec3"; - dnssec-policy "optout"; -}; - -/* The zone will be reconfigured to disable opt-out. */ -zone "nsec3-from-optout.kasp" { - type primary; - file "nsec3-from-optout.kasp.db"; - //dnssec-policy "optout"; - dnssec-policy "nsec3"; -}; - -/* The zone starts with NSEC3, but will be reconfigured to use NSEC. */ -zone "nsec3-to-nsec.kasp" { - type primary; - file "nsec3-to-nsec.kasp.db"; - //dnssec-policy "nsec3"; - dnssec-policy "nsec"; -}; - -/* The zone fails to load, but is fixed after a reload. */ -zone "nsec3-fails-to-load.kasp" { - type primary; - file "nsec3-fails-to-load.kasp.db"; - dnssec-policy "nsec3"; - allow-update { any; }; -}; - -/* These zones switch from dynamic to inline-signing or vice versa. */ -zone "nsec3-dynamic-to-inline.kasp" { - type primary; - file "nsec3-dynamic-to-inline.kasp.db"; - dnssec-policy "nsec3"; - allow-update { any; }; -}; - -zone "nsec3-inline-to-dynamic.kasp" { - type primary; - file "nsec3-inline-to-dynamic.kasp.db"; - inline-signing no; - dnssec-policy "nsec3"; - allow-update { any; }; -}; - -/* - * This zone will have an empty nonterminal node added and a node deleted. - */ -zone "nsec3-ent.kasp" { - type primary; - file "nsec3-ent.kasp.db"; - dnssec-policy "nsec3"; - inline-signing yes; -}; diff --git a/bin/tests/system/nsec3/ns3/named2.conf.in b/bin/tests/system/nsec3/ns3/named2.conf.in deleted file mode 100644 index a883940f31..0000000000 --- a/bin/tests/system/nsec3/ns3/named2.conf.in +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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. - */ - -// NS3 - -include "named-fips.conf"; - -dnssec-policy "rsasha1" { - keys { - csk lifetime unlimited algorithm rsasha1; - }; -}; - -/* - * This zone starts with NSEC, but will be reconfigured to use NSEC3. - * This should work despite the incompatible RSAHSHA1 algorithm, - * because the DS is still in hidden state. - */ -zone "rsasha1-to-nsec3.kasp" { - type primary; - file "rsasha1-to-nsec3.kasp.db"; - //dnssec-policy "rsasha1"; - dnssec-policy "nsec3"; -}; - -/* - * This zone starts with NSEC, but will be reconfigured to use NSEC3. - * This should block because RSASHA1 is not compatible with NSEC3, - * and the DS is published. - */ -zone "rsasha1-to-nsec3-wait.kasp" { - type primary; - file "rsasha1-to-nsec3-wait.kasp.db"; - //dnssec-policy "rsasha1"; - dnssec-policy "nsec3"; -}; - -/* - * This zone starts with NSEC3, but will be reconfigured to use NSEC with an - * NSEC only algorithm. This should work despite the incompatible RSAHSHA1 - * algorithm, because the DS is still in hidden state. - */ -zone "nsec3-to-rsasha1.kasp" { - type primary; - file "nsec3-to-rsasha1.kasp.db"; - //dnssec-policy "nsec3"; - dnssec-policy "rsasha1"; -}; - -/* - * This zone starts with NSEC3, but will be reconfigured to use NSEC with an - * NSEC only algorithm. This should also be fine because we are allowed - * to change to NSEC with any algorithm, then we can also publish the new - * DNSKEY and signatures of the RSASHA1 algorithm. - */ -zone "nsec3-to-rsasha1-ds.kasp" { - type primary; - file "nsec3-to-rsasha1-ds.kasp.db"; - //dnssec-policy "nsec3"; - dnssec-policy "rsasha1"; -}; diff --git a/bin/tests/system/nsec3/setup.sh b/bin/tests/system/nsec3/setup.sh index 56c3ac2eef..1ddb23c55a 100644 --- a/bin/tests/system/nsec3/setup.sh +++ b/bin/tests/system/nsec3/setup.sh @@ -16,19 +16,11 @@ set -e -copy_setports ns2/named.conf.in ns2/named.conf ( cd ns2 $SHELL setup.sh ) -if [ $RSASHA1_SUPPORTED = 0 ]; then - copy_setports ns3/named-fips.conf.in ns3/named.conf -else - copy_setports ns3/named-fips.conf.in ns3/named-fips.conf - # includes named-fips.conf - cp ns3/named1.conf.in ns3/named.conf -fi ( cd ns3 $SHELL setup.sh diff --git a/bin/tests/system/nsec3/tests_nsec3_initial.py b/bin/tests/system/nsec3/tests_nsec3_initial.py index 42fbce461a..7f8b3e2fb9 100644 --- a/bin/tests/system/nsec3/tests_nsec3_initial.py +++ b/bin/tests/system/nsec3/tests_nsec3_initial.py @@ -40,7 +40,10 @@ pytestmark = pytest.mark.extra_artifacts( "ns*/*.jnl", "ns*/*.signed", "ns*/keygen.out.*", + "ns3/named-common.conf", "ns3/named-fips.conf", + "ns3/named-rsasha0.conf", + "ns3/named-rsasha1.conf", ] ) From 85eea3db371a97e04f647489dc1e01d8c2e70b98 Mon Sep 17 00:00:00 2001 From: Matthijs Mekking Date: Tue, 30 Sep 2025 11:48:30 +0200 Subject: [PATCH 03/13] Move parts into a common module Some constants and test functionality are the same for test cases prior and after reconfiguration. Move these into a common module. --- bin/tests/system/nsec3/common.py | 106 ++++++++++++++++++ bin/tests/system/nsec3/tests_nsec3_initial.py | 104 ++--------------- 2 files changed, 116 insertions(+), 94 deletions(-) create mode 100644 bin/tests/system/nsec3/common.py diff --git a/bin/tests/system/nsec3/common.py b/bin/tests/system/nsec3/common.py new file mode 100644 index 0000000000..78c7aabe8f --- /dev/null +++ b/bin/tests/system/nsec3/common.py @@ -0,0 +1,106 @@ +# 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. + +import os + +from datetime import timedelta + +import dns +import pytest + +pytestmark = pytest.mark.extra_artifacts( + [ + "*.axfr", + "*.created", + "dig.out.*", + "rndc.reload.*", + "rndc.signing.*", + "update.out.*", + "verify.out.*", + "ns*/dsset-**", + "ns*/K*", + "ns*/settime.out.*", + "ns*/*.db", + "ns*/*.jbk", + "ns*/*.jnl", + "ns*/*.signed", + "ns*/keygen.out.*", + "ns3/named-common.conf", + "ns3/named-fips.conf", + "ns3/named-rsasha1.conf", + ] +) + +ALGORITHM = os.environ["DEFAULT_ALGORITHM_NUMBER"] +SIZE = os.environ["DEFAULT_BITS"] + +default_config = { + "dnskey-ttl": timedelta(hours=1), + "ds-ttl": timedelta(days=1), + "max-zone-ttl": timedelta(days=1), + "parent-propagation-delay": timedelta(hours=1), + "publish-safety": timedelta(hours=1), + "retire-safety": timedelta(hours=1), + "signatures-refresh": timedelta(days=5), + "signatures-validity": timedelta(days=14), + "zone-propagation-delay": timedelta(minutes=5), +} + + +def check_auth_nsec(response): + rrs = [] + for rrset in response.authority: + if rrset.match(dns.rdataclass.IN, dns.rdatatype.NSEC, dns.rdatatype.NONE): + rrs.append(rrset) + assert not rrset.match( + dns.rdataclass.IN, dns.rdatatype.NSEC3, dns.rdatatype.NONE + ) + assert len(rrs) != 0, "no NSEC records found in authority section" + + +def check_auth_nsec3(response, iterations=0, optout=0, saltlen=0): + match = f"IN NSEC3 1 {optout} {iterations}" + rrs = [] + + for rrset in response.authority: + if rrset.match(dns.rdataclass.IN, dns.rdatatype.NSEC3, dns.rdatatype.NONE): + assert match in rrset.to_text() + if saltlen == 0: + assert f"{match} -" in rrset.to_text() + else: + assert not f"{match} -" in rrset.to_text() + + rrs.append(rrset) + assert not rrset.match( + dns.rdataclass.IN, dns.rdatatype.NSEC, dns.rdatatype.NONE + ) + + assert len(rrs) != 0, "no NSEC3 records found in authority section" + + +def check_nsec3param(response, match, saltlen): + rrs = [] + + for rrset in response.answer: + if rrset.match(dns.rdataclass.IN, dns.rdatatype.NSEC3PARAM, dns.rdatatype.NONE): + assert match in rrset.to_text() + if saltlen == 0: + assert f"{match} -" in rrset.to_text() + else: + assert not f"{match} -" in rrset.to_text() + + rrs.append(rrset) + else: + assert rrset.match( + dns.rdataclass.IN, dns.rdatatype.RRSIG, dns.rdatatype.NSEC3PARAM + ) + + assert len(rrs) != 0 diff --git a/bin/tests/system/nsec3/tests_nsec3_initial.py b/bin/tests/system/nsec3/tests_nsec3_initial.py index 7f8b3e2fb9..25a3070435 100644 --- a/bin/tests/system/nsec3/tests_nsec3_initial.py +++ b/bin/tests/system/nsec3/tests_nsec3_initial.py @@ -9,12 +9,10 @@ # See the COPYRIGHT file distributed with this work for additional # information regarding copyright ownership. +# pylint: disable=redefined-outer-name,unused-import + import shutil -import os -from datetime import timedelta - -import dns import dns.update import pytest @@ -22,58 +20,16 @@ pytest.importorskip("dns", minversion="2.0.0") import isctest import isctest.mark from isctest.vars.algorithms import RSASHA1 - -pytestmark = pytest.mark.extra_artifacts( - [ - "*.axfr", - "*.created", - "dig.out.*", - "rndc.reload.*", - "rndc.signing.*", - "update.out.*", - "verify.out.*", - "ns*/dsset-**", - "ns*/K*", - "ns*/settime.out.*", - "ns*/*.db", - "ns*/*.jbk", - "ns*/*.jnl", - "ns*/*.signed", - "ns*/keygen.out.*", - "ns3/named-common.conf", - "ns3/named-fips.conf", - "ns3/named-rsasha0.conf", - "ns3/named-rsasha1.conf", - ] +from nsec3.common import ( + ALGORITHM, + SIZE, + default_config, + pytestmark, + check_auth_nsec, + check_auth_nsec3, + check_nsec3param, ) -ALGORITHM = os.environ["DEFAULT_ALGORITHM_NUMBER"] -SIZE = os.environ["DEFAULT_BITS"] - -default_config = { - "dnskey-ttl": timedelta(hours=1), - "ds-ttl": timedelta(days=1), - "key-directory": "{keydir}", - "max-zone-ttl": timedelta(days=1), - "parent-propagation-delay": timedelta(hours=1), - "publish-safety": timedelta(hours=1), - "retire-safety": timedelta(hours=1), - "signatures-refresh": timedelta(days=5), - "signatures-validity": timedelta(days=14), - "zone-propagation-delay": timedelta(minutes=5), -} - - -def check_auth_nsec(response): - rrs = [] - for rrset in response.authority: - if rrset.match(dns.rdataclass.IN, dns.rdatatype.NSEC, dns.rdatatype.NONE): - rrs.append(rrset) - assert not rrset.match( - dns.rdataclass.IN, dns.rdatatype.NSEC3, dns.rdatatype.NONE - ) - assert len(rrs) != 0 - @pytest.mark.parametrize( "params", @@ -218,46 +174,6 @@ def wait_for_soa_update(server, fqdn): return verified -def check_nsec3param(response, match, saltlen): - rrs = [] - - for rrset in response.answer: - if rrset.match(dns.rdataclass.IN, dns.rdatatype.NSEC3PARAM, dns.rdatatype.NONE): - assert match in rrset.to_text() - if saltlen == 0: - assert f"{match} -" in rrset.to_text() - else: - assert not f"{match} -" in rrset.to_text() - - rrs.append(rrset) - else: - assert rrset.match( - dns.rdataclass.IN, dns.rdatatype.RRSIG, dns.rdatatype.NSEC3PARAM - ) - - assert len(rrs) != 0 - - -def check_auth_nsec3(response, iterations=0, optout=0, saltlen=0): - match = f"IN NSEC3 1 {optout} {iterations}" - rrs = [] - - for rrset in response.authority: - if rrset.match(dns.rdataclass.IN, dns.rdatatype.NSEC3, dns.rdatatype.NONE): - assert match in rrset.to_text() - if saltlen == 0: - assert f"{match} -" in rrset.to_text() - else: - assert not f"{match} -" in rrset.to_text() - - rrs.append(rrset) - assert not rrset.match( - dns.rdataclass.IN, dns.rdatatype.NSEC, dns.rdatatype.NONE - ) - - assert len(rrs) != 0 - - @pytest.mark.parametrize( "params", [ From ba1ffe56e4f7ab6a4a7714c6551df9f6e42b87cb Mon Sep 17 00:00:00 2001 From: Matthijs Mekking Date: Tue, 30 Sep 2025 12:33:14 +0200 Subject: [PATCH 04/13] Improve check_auth_nsec3 salt checking Since we know the salt after checking the NSEC3PARAM, we might as well check the NSEC3 records on the NXDOMAIN response that the salt matches. --- bin/tests/system/nsec3/common.py | 13 ++++++------- bin/tests/system/nsec3/tests_nsec3_initial.py | 4 ++-- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/bin/tests/system/nsec3/common.py b/bin/tests/system/nsec3/common.py index 78c7aabe8f..12a176fea5 100644 --- a/bin/tests/system/nsec3/common.py +++ b/bin/tests/system/nsec3/common.py @@ -66,18 +66,13 @@ def check_auth_nsec(response): assert len(rrs) != 0, "no NSEC records found in authority section" -def check_auth_nsec3(response, iterations=0, optout=0, saltlen=0): - match = f"IN NSEC3 1 {optout} {iterations}" +def check_auth_nsec3(response, iterations=0, optout=0, salt="-"): + match = f"IN NSEC3 1 {optout} {iterations} {salt}" rrs = [] for rrset in response.authority: if rrset.match(dns.rdataclass.IN, dns.rdatatype.NSEC3, dns.rdatatype.NONE): assert match in rrset.to_text() - if saltlen == 0: - assert f"{match} -" in rrset.to_text() - else: - assert not f"{match} -" in rrset.to_text() - rrs.append(rrset) assert not rrset.match( dns.rdataclass.IN, dns.rdatatype.NSEC, dns.rdatatype.NONE @@ -88,6 +83,7 @@ def check_auth_nsec3(response, iterations=0, optout=0, saltlen=0): def check_nsec3param(response, match, saltlen): rrs = [] + salt = "-" for rrset in response.answer: if rrset.match(dns.rdataclass.IN, dns.rdatatype.NSEC3PARAM, dns.rdatatype.NONE): @@ -96,6 +92,7 @@ def check_nsec3param(response, match, saltlen): assert f"{match} -" in rrset.to_text() else: assert not f"{match} -" in rrset.to_text() + salt = rrset.to_text().split()[7] rrs.append(rrset) else: @@ -104,3 +101,5 @@ def check_nsec3param(response, match, saltlen): ) assert len(rrs) != 0 + + return salt diff --git a/bin/tests/system/nsec3/tests_nsec3_initial.py b/bin/tests/system/nsec3/tests_nsec3_initial.py index 25a3070435..6f5cfcdafc 100644 --- a/bin/tests/system/nsec3/tests_nsec3_initial.py +++ b/bin/tests/system/nsec3/tests_nsec3_initial.py @@ -343,12 +343,12 @@ def test_nsec3_case(ns3, params): response = isctest.query.tcp(query, ns3.ip) assert response.rcode() == dns.rcode.NOERROR - check_nsec3param(response, match, saltlen) + 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, saltlen) + check_auth_nsec3(response, iterations, optout, salt) # Extra test for nsec3-change.kasp. if zone == "nsec3-change.kasp": From 2c7190609b51371b85237419650ecfea8e51d26e Mon Sep 17 00:00:00 2001 From: Matthijs Mekking Date: Tue, 30 Sep 2025 13:25:22 +0200 Subject: [PATCH 05/13] Rewrite nsec3 system test to pytest (2/4) 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 | 158 -------- bin/tests/system/nsec3/tests_nsec3_initial.py | 32 -- .../system/nsec3/tests_nsec3_reconfig.py | 355 ++++++++++++++++++ 3 files changed, 355 insertions(+), 190 deletions(-) create mode 100644 bin/tests/system/nsec3/tests_nsec3_reconfig.py diff --git a/bin/tests/system/nsec3/tests.sh b/bin/tests/system/nsec3/tests.sh index 0414fb15f9..9a4a2d22f7 100644 --- a/bin/tests/system/nsec3/tests.sh +++ b/bin/tests/system/nsec3/tests.sh @@ -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" diff --git a/bin/tests/system/nsec3/tests_nsec3_initial.py b/bin/tests/system/nsec3/tests_nsec3_initial.py index 6f5cfcdafc..99247f8973 100644 --- a/bin/tests/system/nsec3/tests_nsec3_initial.py +++ b/bin/tests/system/nsec3/tests_nsec3_initial.py @@ -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 index 0000000000..7ce5846ed3 --- /dev/null +++ b/bin/tests/system/nsec3/tests_nsec3_reconfig.py @@ -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 From f98f6ee3d68428f1d665b535cdad5cdc6d00d0d6 Mon Sep 17 00:00:00 2001 From: Matthijs Mekking Date: Tue, 30 Sep 2025 15:06:45 +0200 Subject: [PATCH 06/13] Rewrite nsec3 system test to pytest (3/4) This converts two test cases: 1. A zone that previously failed to load is now fixed. Make sure the zone is signed correctly with the right NSEC3 parameters. 2. Test case to ensure the salt is the same after a restart, i.e. no re-salting takes place. Previously we only tested with salt length 0, this commit adds a test case for salt length 8 as well. --- bin/tests/system/nsec3/tests.sh | 38 ------ .../system/nsec3/tests_nsec3_reconfig.py | 3 +- bin/tests/system/nsec3/tests_nsec3_reload.py | 52 ++++++++ bin/tests/system/nsec3/tests_nsec3_restart.py | 119 ++++++++++++++++++ 4 files changed, 173 insertions(+), 39 deletions(-) create mode 100644 bin/tests/system/nsec3/tests_nsec3_reload.py create mode 100644 bin/tests/system/nsec3/tests_nsec3_restart.py diff --git a/bin/tests/system/nsec3/tests.sh b/bin/tests/system/nsec3/tests.sh index 9a4a2d22f7..d22b3fd65f 100644 --- a/bin/tests/system/nsec3/tests.sh +++ b/bin/tests/system/nsec3/tests.sh @@ -235,44 +235,6 @@ key_clear "KEY2" key_clear "KEY3" key_clear "KEY4" -# Test NSEC3 and NSEC3PARAM is the same after restart -set_zone_policy "nsec3.kasp" "nsec3" 1 3600 -set_nsec3param "0" "0" -set_key_default_values "KEY1" -echo_i "check zone ${ZONE} before restart" -check_nsec3 - -# Restart named, NSEC3 should stay the same. -ret=0 -echo "stop ns3" -stop_server --use-rndc --port ${CONTROLPORT} ${DIR} || ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -ret=0 -echo "start ns3" -start_server --noclean --restart --port ${PORT} ${DIR} -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -prevsalt="${SALT}" -set_zone_policy "nsec3.kasp" "nsec3" 1 3600 -set_nsec3param "0" "0" -set_key_default_values "KEY1" -SALT="${prevsalt}" -echo_i "check zone ${ZONE} after restart has salt ${SALT}" -check_nsec3 - -# Zone: nsec3-fails-to-load.kasp. (should be fixed after reload) -cp ns3/template.db.in ns3/nsec3-fails-to-load.kasp.db -rndc_reload ns3 10.53.0.3 - -set_zone_policy "nsec3-fails-to-load.kasp" "nsec3" 1 3600 -set_nsec3param "0" "0" -set_key_default_values "KEY1" -echo_i "check zone ${ZONE} after reload" -check_nsec3 - # Zone: nsec3-ent.kasp (regression test for #5108) n=$((n + 1)) echo_i "check query for newly empty name does not crash ($n)" diff --git a/bin/tests/system/nsec3/tests_nsec3_reconfig.py b/bin/tests/system/nsec3/tests_nsec3_reconfig.py index 7ce5846ed3..9178d9c716 100644 --- a/bin/tests/system/nsec3/tests_nsec3_reconfig.py +++ b/bin/tests/system/nsec3/tests_nsec3_reconfig.py @@ -53,9 +53,10 @@ def after_servers_start(ns3, templates): return False + nsdir = ns3.identifier + # 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") diff --git a/bin/tests/system/nsec3/tests_nsec3_reload.py b/bin/tests/system/nsec3/tests_nsec3_reload.py new file mode 100644 index 0000000000..0e5dd0cfea --- /dev/null +++ b/bin/tests/system/nsec3/tests_nsec3_reload.py @@ -0,0 +1,52 @@ +# 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 pytest + +pytest.importorskip("dns", minversion="2.0.0") +import isctest +from nsec3.common import ( + ALGORITHM, + SIZE, + check_nsec3_case, +) + + +def test_nsec3_case(ns3): + # Get test parameters. + params = { + "zone": "nsec3-fails-to-load.kasp", + "policy": "nsec3", + "key-properties": [ + f"csk 0 {ALGORITHM} {SIZE} goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden", + ], + } + zone = params["zone"] + + # nsec3-fails-to-load.kasp. fails to load (should be fixed after reload). + zone = "nsec3-fails-to-load.kasp" + with ns3.watch_log_from_start() as watcher: + watcher.wait_for_line(f"zone {zone}/IN (unsigned): not loaded due to errors.") + + shutil.copyfile(f"{ns3.identifier}/template.db.in", f"{ns3.identifier}/{zone}.db") + ns3.rndc(f"reload {zone}") + + # First make sure the zone is properly signed. + isctest.kasp.wait_keymgr_done(ns3, zone) + + # Test case. + check_nsec3_case(ns3, params) diff --git a/bin/tests/system/nsec3/tests_nsec3_restart.py b/bin/tests/system/nsec3/tests_nsec3_restart.py new file mode 100644 index 0000000000..9415040eea --- /dev/null +++ b/bin/tests/system/nsec3/tests_nsec3_restart.py @@ -0,0 +1,119 @@ +# 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 dns.update +import pytest + +pytest.importorskip("dns", minversion="2.0.0") +import isctest +import isctest.mark +from nsec3.common import ( + ALGORITHM, + SIZE, + default_config, + pytestmark, + check_auth_nsec3, + check_nsec3param, +) + + +def perform_nsec3_tests(server, params): + # Get test parameters. + zone = params["zone"] + fqdn = f"{zone}." + policy = params["policy"] + keydir = server.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(server, zone) + + keys = isctest.kasp.keydir_to_keylist(zone, keydir) + isctest.kasp.check_keys(zone, keys, expected) + isctest.kasp.check_dnssec_verify(server, zone) + isctest.kasp.check_apex(server, zone, keys, []) + + query = isctest.query.create(fqdn, dns.rdatatype.NSEC3PARAM) + response = isctest.query.tcp(query, server.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, server.ip) + assert response.rcode() == dns.rcode.NXDOMAIN + check_auth_nsec3(response, iterations, optout, salt) + + return salt + + +@pytest.mark.parametrize( + "params", + [ + 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-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): + zone = params["zone"] + salt = perform_nsec3_tests(ns3, params) + + # Test NSEC3 and NSEC3PARAM is the same after restart + isctest.log.info(f"check zone {zone} after restart has salt {salt}") + prevsalt = salt + + # Restart named, NSEC3 should stay the same. + ns3.stop() + ns3.start(["--noclean", "--restart", "--port", os.environ["PORT"]]) + + salt = perform_nsec3_tests(ns3, params) + assert prevsalt == salt From 3f5d295e2983c29415815b7c081191374a34befc Mon Sep 17 00:00:00 2001 From: Matthijs Mekking Date: Tue, 30 Sep 2025 15:11:22 +0200 Subject: [PATCH 07/13] Rewrite nsec3 system test to pytest (4/4) Convert the final nsec3 system test case that deals with empty non-terminals. This is a regression test case for GL #5108. --- .../system/nsec3/ns3/nsec3-ent.kasp.db.j2 | 41 ++++++++ bin/tests/system/nsec3/ns3/setup.sh | 3 +- bin/tests/system/nsec3/tests.sh | 35 ------- .../system/nsec3/tests_nsec3_reconfig.py | 96 +++++++++++++++++++ 4 files changed, 138 insertions(+), 37 deletions(-) create mode 100644 bin/tests/system/nsec3/ns3/nsec3-ent.kasp.db.j2 diff --git a/bin/tests/system/nsec3/ns3/nsec3-ent.kasp.db.j2 b/bin/tests/system/nsec3/ns3/nsec3-ent.kasp.db.j2 new file mode 100644 index 0000000000..5348dcfafa --- /dev/null +++ b/bin/tests/system/nsec3/ns3/nsec3-ent.kasp.db.j2 @@ -0,0 +1,41 @@ +; 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. + +{% set serial = serial | default(1) %} + +$ORIGIN nsec3-ent.kasp. +$TTL 300 +nsec3-ent.kasp. IN SOA mname1. . ( + @serial@ ; serial + 20 ; refresh (20 seconds) + 20 ; retry (20 seconds) + 1814400 ; expire (3 weeks) + 3600 ; minimum (1 hour) + ) + + NS ns3 +ns3 A 10.53.0.3 + +a A 10.0.0.1 +b A 10.0.0.2 + +{% if serial == 1 %} +c A 10.0.0.3 +{% endif %} + +{% if serial == 2 %} +d A 10.0.0.3 +{% endif %} + +{% if serial == 3 %} +c A 10.0.0.3 +x.y.z A 10.0.0.4 +{% endif %} diff --git a/bin/tests/system/nsec3/ns3/setup.sh b/bin/tests/system/nsec3/ns3/setup.sh index 32ddf5e9c5..5b7053e08d 100644 --- a/bin/tests/system/nsec3/ns3/setup.sh +++ b/bin/tests/system/nsec3/ns3/setup.sh @@ -26,8 +26,7 @@ setup() { for zn in nsec-to-nsec3 nsec3 nsec3-other nsec3-change nsec3-to-nsec \ nsec3-to-optout nsec3-from-optout nsec3-dynamic \ nsec3-dynamic-change nsec3-dynamic-to-inline \ - nsec3-inline-to-dynamic nsec3-dynamic-update-inline \ - nsec3-ent; do + nsec3-inline-to-dynamic nsec3-dynamic-update-inline; do setup "${zn}.kasp" done diff --git a/bin/tests/system/nsec3/tests.sh b/bin/tests/system/nsec3/tests.sh index d22b3fd65f..131daf052e 100644 --- a/bin/tests/system/nsec3/tests.sh +++ b/bin/tests/system/nsec3/tests.sh @@ -235,40 +235,5 @@ key_clear "KEY2" key_clear "KEY3" key_clear "KEY4" -# Zone: nsec3-ent.kasp (regression test for #5108) -n=$((n + 1)) -echo_i "check query for newly empty name does not crash ($n)" -set_zone_policy "nsec3-ent.kasp" -set_server "ns3" "10.53.0.3" -# confirm the pre-existing name still exists -dig_with_opts +noquestion "@${SERVER}" c.$ZONE >"dig.out.$ZONE.test$n.1" || ret=1 -grep "c\.nsec3-ent\.kasp\..*IN.*A.*10\.0\.0\.3" "dig.out.$ZONE.test$n.1" >/dev/null || ret=1 -# remove a name, bump the SOA, and reload -sed -e 's/1 *; serial/2/' -e '/^c/d' ns3/template.db.in >ns3/nsec3-ent.kasp.db -rndc_reload ns3 10.53.0.3 -# try the query again -dig_with_opts +noquestion "@${SERVER}" c.$ZONE >"dig.out.$ZONE.test$n.2" || ret=1 -grep "status: NXDOMAIN" "dig.out.$ZONE.test$n.2" >/dev/null || ret=1 -if [ "$ret" -ne 0 ]; then echo_i "failed"; fi -status=$((status + ret)) - -n=$((n + 1)) -echo_i "check queries for new names below ENT do not crash ($n)" -set_zone_policy "nsec3-ent.kasp" -set_server "ns3" "10.53.0.3" -# confirm the ENT name does not exist yet -dig_with_opts +noquestion "@${SERVER}" x.y.z.$ZONE >"dig.out.$ZONE.test$n.1" || ret=1 -grep "status: NXDOMAIN" "dig.out.$ZONE.test$n.1" >/dev/null || ret=1 -# add a name with an ENT, bump the SOA, and reload ensuring the time stamp changes -sleep 1 -sed -e 's/1 *; serial/3/' ns3/template.db.in >ns3/nsec3-ent.kasp.db -echo "x.y.z A 10.0.0.4" >>ns3/nsec3-ent.kasp.db -rndc_reload ns3 10.53.0.3 -# try the query again -dig_with_opts +noquestion "@${SERVER}" x.y.z.$ZONE >"dig.out.$ZONE.test$n.2" || ret=1 -grep "x\.y\.z\.nsec3-ent\.kasp\..*IN.*A.*10\.0\.0\.4" "dig.out.$ZONE.test$n.2" >/dev/null || ret=1 -if [ "$ret" -ne 0 ]; then echo_i "failed"; fi -status=$((status + ret)) - echo_i "exit status: $status" [ $status -eq 0 ] || exit 1 diff --git a/bin/tests/system/nsec3/tests_nsec3_reconfig.py b/bin/tests/system/nsec3/tests_nsec3_reconfig.py index 9178d9c716..5dd27e281a 100644 --- a/bin/tests/system/nsec3/tests_nsec3_reconfig.py +++ b/bin/tests/system/nsec3/tests_nsec3_reconfig.py @@ -354,3 +354,99 @@ def test_nsec3_case(ns3, params): ) response = ns3.rndc(f"signing -nsec3param 1 1 12 ffff {zone}") assert "zone uses dnssec-policy, use rndc dnssec command instead" in response + + +def test_nsec3_ent(ns3, templates): + # Zone: nsec3-ent.kasp (regression test for #5108) + zone = "nsec3-ent.kasp" + fqdn = f"{zone}." + policy = "nsec3" + keydir = ns3.identifier + config = default_config + ttl = int(config.get("dnskey-ttl", 3600).total_seconds()) + minimum = 3600 + keyprops = [ + f"csk 0 {ALGORITHM} {SIZE} goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden", + ] + expected = isctest.kasp.policy_to_properties(ttl=ttl, keys=keyprops) + + # 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, ns3.ports.dns, timeout=3) + assert response.rcode() == dns.rcode.NOERROR + + match = f"{fqdn} {minimum} IN NSEC3PARAM 1 0 0" + salt = check_nsec3param(response, match, 0) + + query = isctest.query.create(f"nosuchname.{fqdn}", dns.rdatatype.A) + response = isctest.query.tcp(query, ns3.ip, ns3.ports.dns, timeout=3) + assert response.rcode() == dns.rcode.NXDOMAIN + check_auth_nsec3(response, 0, 0, salt) + + isctest.log.info("check query for newly empty name does not crash") + + # confirm the pre-existing name still exists + query = isctest.query.create(f"c.{fqdn}", dns.rdatatype.A) + response = isctest.query.tcp(query, ns3.ip, ns3.ports.dns, timeout=3) + assert response.rcode() == dns.rcode.NOERROR + + match = "10.0.0.3" + rrset = response.get_rrset( + response.answer, + dns.name.from_text(f"c.{fqdn}"), + dns.rdataclass.IN, + dns.rdatatype.A, + ) + assert rrset is not None, "no A records found in answer section" + assert match in str(rrset[0]) + + # remove a name, bump the SOA, and reload + templates.render(f"{ns3.identifier}/nsec3-ent.kasp.db", {"serial": 2}) + + with ns3.watch_log_from_here() as watcher: + ns3.rndc(f"reload {zone}") + watcher.wait_for_line(f"zone {zone}/IN (signed): sending notifies") + + # try the query again + query = isctest.query.create(f"c.{fqdn}", dns.rdatatype.A) + response = isctest.query.tcp(query, ns3.ip, ns3.ports.dns, timeout=3) + assert response.rcode() == dns.rcode.NXDOMAIN + + isctest.log.info("check queries for new names below ENT do not crash") + + # confirm the ENT name does not exist yet + query = isctest.query.create(f"x.y.z.{fqdn}", dns.rdatatype.A) + response = isctest.query.tcp(query, ns3.ip, ns3.ports.dns, timeout=3) + assert response.rcode() == dns.rcode.NXDOMAIN + + # add a name with an ENT, bump the SOA, and reload ensuring the time stamp changes + templates.render(f"{ns3.identifier}/nsec3-ent.kasp.db", {"serial": 3}) + + with ns3.watch_log_from_here() as watcher: + ns3.rndc(f"reload {zone}") + watcher.wait_for_line(f"zone {zone}/IN (signed): sending notifies") + + # try the query again + query = isctest.query.create(f"x.y.z.{fqdn}", dns.rdatatype.A) + response = isctest.query.tcp(query, ns3.ip, ns3.ports.dns, timeout=3) + assert response.rcode() == dns.rcode.NOERROR + + match = "10.0.0.4" + rrset = response.get_rrset( + response.answer, + dns.name.from_text(f"x.y.z.{fqdn}"), + dns.rdataclass.IN, + dns.rdatatype.A, + ) + assert rrset is not None, "no A records found in answer section" + assert match in str(rrset[0]) From 29816eb621644f41c48e13672c301beb2fa6b6ef Mon Sep 17 00:00:00 2001 From: Matthijs Mekking Date: Tue, 30 Sep 2025 15:12:45 +0200 Subject: [PATCH 08/13] Clean up shell tests remnants Now that all tests are covered by pytest, we can remove the remnants of the shell test script. --- bin/tests/system/nsec3/tests.sh | 239 ----------------------- bin/tests/system/nsec3/tests_sh_nsec3.py | 36 ---- 2 files changed, 275 deletions(-) delete mode 100644 bin/tests/system/nsec3/tests.sh delete mode 100644 bin/tests/system/nsec3/tests_sh_nsec3.py diff --git a/bin/tests/system/nsec3/tests.sh b/bin/tests/system/nsec3/tests.sh deleted file mode 100644 index 131daf052e..0000000000 --- a/bin/tests/system/nsec3/tests.sh +++ /dev/null @@ -1,239 +0,0 @@ -#!/bin/sh - -# 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. - -set -e - -# shellcheck source=conf.sh -. ../conf.sh -# shellcheck source=kasp.sh -. ../kasp.sh - -# Log errors and increment $ret. -log_error() { - echo_i "error: $1" - ret=$((ret + 1)) -} - -# Call dig with default options. -dig_with_opts() { - $DIG +tcp +noadd +nosea +nostat +nocmd +dnssec -p "$PORT" "$@" -} - -# Call rndc. -rndccmd() { - "$RNDC" -c ../_common/rndc.conf -p "$CONTROLPORT" -s "$@" -} - -# Set zone name ($1) and policy ($2) for testing nsec3. -# Also set the expected number of keys ($3) and DNSKEY TTL ($4). -set_zone_policy() { - ZONE=$1 - POLICY=$2 - NUM_KEYS=$3 - DNSKEY_TTL=$4 - KEYFILE_TTL=$4 - # The CDS digest type in these tests are all the default, - # which is SHA-256 (2). - CDS_SHA256="yes" - CDS_SHA384="no" -} -# Set expected NSEC3 parameters: flags ($1) and salt length ($2). -set_nsec3param() { - FLAGS=$1 - SALTLEN=$2 - # Reset salt. - SALT="" -} - -# Set expected default dnssec-policy keys values. -set_key_default_values() { - key_clear $1 - - set_keyrole $1 "csk" - set_keylifetime $1 "0" - set_keyalgorithm $1 "13" "ECDSAP256SHA256" "256" - set_keysigning $1 "yes" - set_zonesigning $1 "yes" - - set_keystate $1 "GOAL" "omnipresent" - set_keystate $1 "STATE_DNSKEY" "rumoured" - set_keystate $1 "STATE_KRRSIG" "rumoured" - set_keystate $1 "STATE_ZRRSIG" "rumoured" - set_keystate $1 "STATE_DS" "hidden" -} - -# Set expected rsasha1 dnssec-policy keys values. -set_key_rsasha1_values() { - key_clear $1 - - set_keyrole $1 "csk" - set_keylifetime $1 "0" - set_keyalgorithm $1 "5" "RSASHA1" "2048" - set_keysigning $1 "yes" - set_zonesigning $1 "yes" - - set_keystate $1 "GOAL" "omnipresent" - set_keystate $1 "STATE_DNSKEY" "rumoured" - set_keystate $1 "STATE_KRRSIG" "rumoured" - set_keystate $1 "STATE_ZRRSIG" "rumoured" - set_keystate $1 "STATE_DS" "hidden" -} - -# Update the key states. -set_key_states() { - set_keystate $1 "GOAL" "$2" - set_keystate $1 "STATE_DNSKEY" "$3" - set_keystate $1 "STATE_KRRSIG" "$4" - set_keystate $1 "STATE_ZRRSIG" "$5" - set_keystate $1 "STATE_DS" "$6" -} - -# The apex NSEC3PARAM record indicates that it is signed. -_wait_for_nsec3param() { - dig_with_opts +noquestion "@${SERVER}" "$ZONE" NSEC3PARAM >"dig.out.test$n.wait" || return 1 - grep "${ZONE}\..*IN.*NSEC3PARAM 1 0 0.*${SALT}" "dig.out.test$n.wait" >/dev/null || return 1 - grep "${ZONE}\..*IN.*RRSIG" "dig.out.test$n.wait" >/dev/null || return 1 - return 0 -} -# The apex NSEC record indicates that it is signed. -_wait_for_nsec() { - dig_with_opts +noquestion "@${SERVER}" "$ZONE" NSEC >"dig.out.test$n.wait" || return 1 - grep "NS SOA" "dig.out.test$n.wait" >/dev/null || return 1 - grep "${ZONE}\..*IN.*RRSIG" "dig.out.test$n.wait" >/dev/null || return 1 - grep "${ZONE}\..*IN.*NSEC3PARAM" "dig.out.test$n.wait" >/dev/null && return 1 - return 0 -} - -# Wait for the zone to be signed. -wait_for_zone_is_signed() { - n=$((n + 1)) - ret=0 - echo_i "wait for ${ZONE} to be signed with $1 ($n)" - - if [ "$1" = "nsec3" ]; then - retry_quiet 10 _wait_for_nsec3param || log_error "wait for ${ZONE} to be signed failed" - else - retry_quiet 10 _wait_for_nsec || log_error "wait for ${ZONE} to be signed failed" - fi - - test "$ret" -eq 0 || echo_i "failed" - status=$((status + ret)) -} - -# Test: check DNSSEC verify -_check_dnssec_verify() { - dig_with_opts @$SERVER "${ZONE}" AXFR >"dig.out.test$n.axfr.$ZONE" || return 1 - $VERIFY -z -o "$ZONE" "dig.out.test$n.axfr.$ZONE" >"verify.out.test$n.$ZONE" 2>&1 || return 1 - return 0 -} - -# Test: check NSEC in answers -_check_nsec_nsec3param() { - dig_with_opts +noquestion @$SERVER "${ZONE}" NSEC3PARAM >"dig.out.test$n.nsec3param.$ZONE" || return 1 - grep "NSEC3PARAM" "dig.out.test$n.nsec3param.$ZONE" >/dev/null && return 1 - return 0 -} - -_check_nsec_nxdomain() { - dig_with_opts @$SERVER "nosuchname.${ZONE}" >"dig.out.test$n.nxdomain.$ZONE" || return 1 - grep "${ZONE}.*IN.*NSEC.*NS.*SOA.*RRSIG.*NSEC.*DNSKEY" "dig.out.test$n.nxdomain.$ZONE" >/dev/null || return 1 - grep "NSEC3" "dig.out.test$n.nxdomain.$ZONE" >/dev/null && return 1 - return 0 -} - -check_nsec() { - wait_for_zone_is_signed "nsec" - - n=$((n + 1)) - echo_i "check DNSKEY rrset is signed correctly for zone ${ZONE} ($n)" - ret=0 - check_keys - retry_quiet 10 _check_apex_dnskey || log_error "bad DNSKEY RRset for zone ${ZONE}" - test "$ret" -eq 0 || echo_i "failed" - status=$((status + ret)) - - n=$((n + 1)) - echo_i "verify DNSSEC for zone ${ZONE} ($n)" - ret=0 - retry_quiet 10 _check_dnssec_verify || log_error "DNSSEC verify failed for zone ${ZONE}" - test "$ret" -eq 0 || echo_i "failed" - status=$((status + ret)) - - n=$((n + 1)) - echo_i "check NSEC3PARAM response for zone ${ZONE} ($n)" - ret=0 - retry_quiet 10 _check_nsec_nsec3param || log_error "unexpected NSEC3PARAM in response for zone ${ZONE}" - test "$ret" -eq 0 || echo_i "failed" - status=$((status + ret)) - - n=$((n + 1)) - echo_i "check NXDOMAIN response for zone ${ZONE} ($n)" - ret=0 - retry_quiet 10 _check_nsec_nxdomain || log_error "bad NXDOMAIN response for zone ${ZONE}" - test "$ret" -eq 0 || echo_i "failed" - status=$((status + ret)) -} - -# Test: check NSEC3 parameters in answers -_check_nsec3_nsec3param() { - dig_with_opts +noquestion @$SERVER "${ZONE}" NSEC3PARAM >"dig.out.test$n.nsec3param.$ZONE" || return 1 - grep "${ZONE}.*0.*IN.*NSEC3PARAM.*1.*0.*0.*${SALT}" "dig.out.test$n.nsec3param.$ZONE" >/dev/null || return 1 - - if [ -z "$SALT" ]; then - SALT=$(awk '$4 == "NSEC3PARAM" { print $8 }' dig.out.test$n.nsec3param.$ZONE) - fi - return 0 -} - -_check_nsec3_nxdomain() { - dig_with_opts @$SERVER "nosuchname.${ZONE}" >"dig.out.test$n.nxdomain.$ZONE" || return 1 - grep ".*\.${ZONE}.*IN.*NSEC3.*1.${FLAGS}.*0.*${SALT}" "dig.out.test$n.nxdomain.$ZONE" >/dev/null || return 1 - return 0 -} - -check_nsec3() { - wait_for_zone_is_signed "nsec3" - - n=$((n + 1)) - echo_i "check that NSEC3PARAM 1 0 0 ${SALT} is published zone ${ZONE} ($n)" - ret=0 - retry_quiet 10 _check_nsec3_nsec3param || log_error "bad NSEC3PARAM response for ${ZONE}" - test "$ret" -eq 0 || echo_i "failed" - status=$((status + ret)) - - n=$((n + 1)) - echo_i "check NXDOMAIN response has correct NSEC3 1 ${FLAGS} 0 ${SALT} for zone ${ZONE} ($n)" - ret=0 - retry_quiet 10 _check_nsec3_nxdomain || log_error "bad NXDOMAIN response for zone ${ZONE}" - test "$ret" -eq 0 || echo_i "failed" - status=$((status + ret)) - - n=$((n + 1)) - echo_i "verify DNSSEC for zone ${ZONE} ($n)" - ret=0 - retry_quiet 10 _check_dnssec_verify || log_error "DNSSEC verify failed for zone ${ZONE}" - test "$ret" -eq 0 || echo_i "failed" - status=$((status + ret)) -} - -start_time="$(TZ=UTC date +%s)" -status=0 -n=0 - -key_clear "KEY1" -key_clear "KEY2" -key_clear "KEY3" -key_clear "KEY4" - -echo_i "exit status: $status" -[ $status -eq 0 ] || exit 1 diff --git a/bin/tests/system/nsec3/tests_sh_nsec3.py b/bin/tests/system/nsec3/tests_sh_nsec3.py deleted file mode 100644 index 90476f2518..0000000000 --- a/bin/tests/system/nsec3/tests_sh_nsec3.py +++ /dev/null @@ -1,36 +0,0 @@ -# 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. - -import pytest - -pytestmark = pytest.mark.extra_artifacts( - [ - "*.created", - "dig.out.*", - "rndc.reload.*", - "rndc.signing.*", - "update.out.*", - "verify.out.*", - "ns*/dsset-**", - "ns*/K*", - "ns*/settime.out.*", - "ns*/*.db", - "ns*/*.jbk", - "ns*/*.jnl", - "ns*/*.signed", - "ns*/keygen.out.*", - "ns3/named-fips.conf", - ] -) - - -def test_nsec3(run_tests_sh): - run_tests_sh() From 7762b2391e1e4e3a24045de4e2717aa4822ddd2f Mon Sep 17 00:00:00 2001 From: Matthijs Mekking Date: Thu, 13 Nov 2025 19:54:03 +0100 Subject: [PATCH 09/13] Move check_nsec3_case to common code With the nsec and nsec3 test cases being nearly identical for all modules, these can be be unified and moved to common.py. --- bin/tests/system/nsec3/common.py | 62 ++++++++++ bin/tests/system/nsec3/tests_nsec3_initial.py | 76 +----------- .../system/nsec3/tests_nsec3_reconfig.py | 110 +++--------------- bin/tests/system/nsec3/tests_nsec3_restart.py | 46 +++----- 4 files changed, 97 insertions(+), 197 deletions(-) diff --git a/bin/tests/system/nsec3/common.py b/bin/tests/system/nsec3/common.py index 12a176fea5..678cc4cbed 100644 --- a/bin/tests/system/nsec3/common.py +++ b/bin/tests/system/nsec3/common.py @@ -16,6 +16,8 @@ from datetime import timedelta import dns import pytest +import isctest + pytestmark = pytest.mark.extra_artifacts( [ "*.axfr", @@ -103,3 +105,63 @@ def check_nsec3param(response, match, saltlen): assert len(rrs) != 0 return salt + + +def check_nsec3_case(server, params, nsec3=True): + # Get test parameters. + zone = params["zone"] + fqdn = f"{zone}." + policy = params["policy"] + keydir = server.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}") + + # Key files. + keys = isctest.kasp.keydir_to_keylist(zone, keydir) + if "external-keys" in params: + expected2 = isctest.kasp.policy_to_properties(ttl, keys=params["external-keys"]) + for ek in expected2: + ek.private = False # noqa + ek.legacy = True # noqa + expected = expected + expected2 + assert "external-keydir" in params + extkeys = isctest.kasp.keydir_to_keylist(zone, params["external-keydir"]) + keys = keys + extkeys + + isctest.kasp.check_keys(zone, keys, expected) + isctest.kasp.check_dnssec_verify(server, zone) + isctest.kasp.check_apex(server, zone, keys, []) + + query = isctest.query.create(fqdn, dns.rdatatype.NSEC3PARAM) + nsec3param_response = isctest.query.tcp(query, server.ip) + assert nsec3param_response.rcode() == dns.rcode.NOERROR + + query = isctest.query.create(f"nosuchname.{fqdn}", dns.rdatatype.A) + response = isctest.query.tcp(query, server.ip) + assert response.rcode() == dns.rcode.NXDOMAIN + + if nsec3: + # NSEC3 + minimum = params.get("soa-minimum", 3600) + 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}" + + salt = check_nsec3param(nsec3param_response, match, saltlen) + + check_auth_nsec3(response, iterations, optout, salt) + else: + # NSEC + assert len(nsec3param_response.answer) == 0 + check_auth_nsec(nsec3param_response) + + check_auth_nsec(response) diff --git a/bin/tests/system/nsec3/tests_nsec3_initial.py b/bin/tests/system/nsec3/tests_nsec3_initial.py index 99247f8973..a8f52350fe 100644 --- a/bin/tests/system/nsec3/tests_nsec3_initial.py +++ b/bin/tests/system/nsec3/tests_nsec3_initial.py @@ -23,9 +23,7 @@ from nsec3.common import ( SIZE, default_config, pytestmark, - check_auth_nsec, - check_auth_nsec3, - check_nsec3param, + check_nsec3_case, ) @@ -95,45 +93,12 @@ from nsec3.common import ( 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) - # Key files. - keys = isctest.kasp.keydir_to_keylist(zone, keydir) - if "external-keys" in params: - expected2 = isctest.kasp.policy_to_properties(ttl, keys=params["external-keys"]) - for ek in expected2: - ek.private = False # noqa - ek.legacy = True # noqa - expected = expected + expected2 - assert "external-keydir" in params - extkeys = isctest.kasp.keydir_to_keylist(zone, params["external-keydir"]) - keys = keys + extkeys - - 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) + # Test case. + check_nsec3_case(ns3, params, nsec3=False) # Extra test for nsec3-dynamic-update-inline.kasp. if zone == "nsec3-dynamic-update-inline.kasp": @@ -291,40 +256,9 @@ def test_nsec_case(ns3, params): 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["dnskey-ttl"].total_seconds()) - 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} 3600 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) - 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) + # Test case. + check_nsec3_case(ns3, params) diff --git a/bin/tests/system/nsec3/tests_nsec3_reconfig.py b/bin/tests/system/nsec3/tests_nsec3_reconfig.py index 5dd27e281a..3525a26444 100644 --- a/bin/tests/system/nsec3/tests_nsec3_reconfig.py +++ b/bin/tests/system/nsec3/tests_nsec3_reconfig.py @@ -27,9 +27,7 @@ from nsec3.common import ( SIZE, default_config, pytestmark, - check_auth_nsec, - check_auth_nsec3, - check_nsec3param, + check_nsec3_case, ) @@ -130,38 +128,13 @@ def after_servers_start(ns3, templates): ], ) 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) + # Test case. + check_nsec3_case(ns3, params, nsec3=False) @pytest.mark.parametrize( @@ -307,44 +280,12 @@ def test_nsec_case(ns3, params): 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) + # Test case. + check_nsec3_case(ns3, params) # Extra test for nsec3-change.kasp. if zone == "nsec3-change.kasp": @@ -358,41 +299,24 @@ def test_nsec3_case(ns3, params): def test_nsec3_ent(ns3, templates): # Zone: nsec3-ent.kasp (regression test for #5108) - zone = "nsec3-ent.kasp" - fqdn = f"{zone}." - policy = "nsec3" - keydir = ns3.identifier - config = default_config - ttl = int(config.get("dnskey-ttl", 3600).total_seconds()) - minimum = 3600 - keyprops = [ - f"csk 0 {ALGORITHM} {SIZE} goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden", - ] - expected = isctest.kasp.policy_to_properties(ttl=ttl, keys=keyprops) + params = { + "zone": "nsec3-ent.kasp", + "policy": "nsec3", + "key-properties": [ + f"csk 0 {ALGORITHM} {SIZE} goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden", + ], + } - # Test case. - isctest.log.info(f"check nsec3 case zone {zone} policy {policy}") + zone = params["zone"] + fqdn = f"{zone}." # 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, ns3.ports.dns, timeout=3) - assert response.rcode() == dns.rcode.NOERROR - - match = f"{fqdn} {minimum} IN NSEC3PARAM 1 0 0" - salt = check_nsec3param(response, match, 0) - - query = isctest.query.create(f"nosuchname.{fqdn}", dns.rdatatype.A) - response = isctest.query.tcp(query, ns3.ip, ns3.ports.dns, timeout=3) - assert response.rcode() == dns.rcode.NXDOMAIN - check_auth_nsec3(response, 0, 0, salt) + # Test case. + check_nsec3_case(ns3, params) + # Test empty non-terminals do not trigger a crash. isctest.log.info("check query for newly empty name does not crash") # confirm the pre-existing name still exists diff --git a/bin/tests/system/nsec3/tests_nsec3_restart.py b/bin/tests/system/nsec3/tests_nsec3_restart.py index 9415040eea..cae21f4413 100644 --- a/bin/tests/system/nsec3/tests_nsec3_restart.py +++ b/bin/tests/system/nsec3/tests_nsec3_restart.py @@ -24,7 +24,7 @@ from nsec3.common import ( SIZE, default_config, pytestmark, - check_auth_nsec3, + check_nsec3_case, check_nsec3param, ) @@ -33,45 +33,25 @@ def perform_nsec3_tests(server, params): # Get test parameters. zone = params["zone"] fqdn = f"{zone}." - policy = params["policy"] - keydir = server.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(server, zone) - keys = isctest.kasp.keydir_to_keylist(zone, keydir) - isctest.kasp.check_keys(zone, keys, expected) - isctest.kasp.check_dnssec_verify(server, zone) - isctest.kasp.check_apex(server, zone, keys, []) + # Test case. + check_nsec3_case(server, params) + + # Return salt. + minimum = params.get("soa-minimum", 3600) + iterations = 0 + saltlen = 0 + if "nsec3param" in params: + saltlen = params["nsec3param"].get("salt-length", 0) + + match = f"{fqdn} {minimum} IN NSEC3PARAM 1 0 {iterations}" query = isctest.query.create(fqdn, dns.rdatatype.NSEC3PARAM) response = isctest.query.tcp(query, server.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, server.ip) - assert response.rcode() == dns.rcode.NXDOMAIN - check_auth_nsec3(response, iterations, optout, salt) - - return salt + return check_nsec3param(response, match, saltlen) @pytest.mark.parametrize( From 9ee62467a6f2789eb0062df4fa41ae68b4e724d8 Mon Sep 17 00:00:00 2001 From: Matthijs Mekking Date: Fri, 14 Nov 2025 16:31:42 +0100 Subject: [PATCH 10/13] Move nsec3-change.kasp test to separate module This zone has a specific corner case to be tested during the setup. Move it to a separate module so it is less convoluted. --- bin/tests/system/nsec3/tests_nsec3_change.py | 101 ++++++++++++++++++ .../system/nsec3/tests_nsec3_reconfig.py | 58 +--------- 2 files changed, 103 insertions(+), 56 deletions(-) create mode 100644 bin/tests/system/nsec3/tests_nsec3_change.py diff --git a/bin/tests/system/nsec3/tests_nsec3_change.py b/bin/tests/system/nsec3/tests_nsec3_change.py new file mode 100644 index 0000000000..39b3a1cbfe --- /dev/null +++ b/bin/tests/system/nsec3/tests_nsec3_change.py @@ -0,0 +1,101 @@ +# 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_nsec3_case, +) + + +@pytest.fixture(scope="module", autouse=True) +def after_servers_start(ns3, templates): + + def check_soa_update(): + query = isctest.query.create(fqdn, dns.rdatatype.SOA) + response = isctest.query.tcp(query, ns3.ip, attempts=1, timeout=2) + rrset = response.get_rrset( + response.answer, + dns.name.from_text(fqdn), + dns.rdataclass.IN, + dns.rdatatype.SOA, + ) + return rrset[0].minimum == 900 + + nsdir = ns3.identifier + + zone = "nsec3-change.kasp" + 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(check_soa_update, timeout=10) + # After reconfig, the NSEC3PARAM TTL should match the new SOA MINIMUM. + + # Reconfigure. + templates.render(f"{nsdir}/named-fips.conf", {"reconfiged": True}) + templates.render(f"{nsdir}/named-rsasha1.conf", {"reconfiged": True}) + + # Wait for the NSEC3 chain is finished rebuilding. + messages = [ + f"zone {zone}/IN (signed): generated salt", + f"zone_nsec3chain: zone {zone}/IN (signed): enter", + f"add {zone}. 900 IN NSEC3PARAM 1 0 0", + f"zone_needdump: zone {zone}/IN (signed): enter", + ] + with ns3.watch_log_from_start() as watcher: + ns3.reconfigure() + watcher.wait_for_sequence(messages) + + +def test_nsec3_case(ns3): + # Get test parameters. + params = { + "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", + ], + } + zone = params["zone"] + + # First make sure the zone is properly signed. + isctest.kasp.wait_keymgr_done(ns3, zone, reconfig=True) + + # Test case. + check_nsec3_case(ns3, params) + + # 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 diff --git a/bin/tests/system/nsec3/tests_nsec3_reconfig.py b/bin/tests/system/nsec3/tests_nsec3_reconfig.py index 3525a26444..1f7ffec73e 100644 --- a/bin/tests/system/nsec3/tests_nsec3_reconfig.py +++ b/bin/tests/system/nsec3/tests_nsec3_reconfig.py @@ -33,36 +33,6 @@ from nsec3.common import ( @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 - - nsdir = ns3.identifier - - # Extra test for nsec3-change.kasp. - zone = "nsec3-change.kasp" - 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" @@ -71,8 +41,8 @@ def after_servers_start(ns3, templates): 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}) + templates.render(f"{ns3.identifier}/named-fips.conf", {"reconfiged": True}) + templates.render(f"{ns3.identifier}/named-rsasha1.conf", {"reconfiged": True}) ns3.reconfigure() @@ -182,21 +152,6 @@ def test_nsec_case(ns3, params): }, 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", @@ -287,15 +242,6 @@ def test_nsec3_case(ns3, params): # Test case. check_nsec3_case(ns3, params) - # 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 - def test_nsec3_ent(ns3, templates): # Zone: nsec3-ent.kasp (regression test for #5108) From 66e4146a88c4de2b0881e29ff269f01f6658d376 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicki=20K=C5=99=C3=AD=C5=BEek?= Date: Thu, 20 Nov 2025 18:09:58 +0100 Subject: [PATCH 11/13] Only render required zones in config for nsec3 tests When all zones are configured, regardless of whether the test module actually uses them, it makes debugging the logs needlessly more complicated, as there is a bunch of stuff going on that is completely unrelated to the test. Define a list of tested zones in each test module and only render the named.conf with those zones defined. --- bin/tests/system/nsec3/ns2/named.conf.j2 | 2 + bin/tests/system/nsec3/ns3/named-fips.conf.j2 | 83 ++++++++++++------- .../system/nsec3/ns3/named-rsasha1.conf.j2 | 9 ++ bin/tests/system/nsec3/tests_nsec3_change.py | 20 ++++- bin/tests/system/nsec3/tests_nsec3_initial.py | 36 ++++++++ .../system/nsec3/tests_nsec3_reconfig.py | 40 ++++++++- bin/tests/system/nsec3/tests_nsec3_reload.py | 12 +++ bin/tests/system/nsec3/tests_nsec3_restart.py | 13 +++ 8 files changed, 183 insertions(+), 32 deletions(-) diff --git a/bin/tests/system/nsec3/ns2/named.conf.j2 b/bin/tests/system/nsec3/ns2/named.conf.j2 index 924e9d26cb..904abbf81d 100644 --- a/bin/tests/system/nsec3/ns2/named.conf.j2 +++ b/bin/tests/system/nsec3/ns2/named.conf.j2 @@ -39,8 +39,10 @@ controls { inet 10.53.0.2 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; }; +{% if "nsec3-xfr-inline.kasp" in zones %} zone "nsec3-xfr-inline.kasp" { type primary; file "nsec3-xfr-inline.kasp.db"; dnssec-policy "nsec3"; }; +{% endif %}{# nsec3-xfr-inline.kasp #} diff --git a/bin/tests/system/nsec3/ns3/named-fips.conf.j2 b/bin/tests/system/nsec3/ns3/named-fips.conf.j2 index 5029457598..8074646fa6 100644 --- a/bin/tests/system/nsec3/ns3/named-fips.conf.j2 +++ b/bin/tests/system/nsec3/ns3/named-fips.conf.j2 @@ -18,41 +18,52 @@ {% set nsec3_from_optout = "optout" if not reconfiged else "nsec3" %} {% set nsec3_to_optout = "nsec3" if not reconfiged else "optout" %} +{% if "nsec-to-nsec3.kasp" in zones %} /* This zone starts with NSEC, but will be reconfigured to use NSEC3. */ zone "nsec-to-nsec3.kasp" { type primary; file "nsec-to-nsec3.kasp.db"; dnssec-policy "@nsec_to_nsec3@"; }; +{% endif %}{# nsec-to-nsec3.kasp #} +{% if "nsec3.kasp" in zones %} /* These zones use the default NSEC3 settings. */ zone "nsec3.kasp" { type primary; file "nsec3.kasp.db"; dnssec-policy "nsec3"; }; +{% endif %}{# nsec3.kasp #} +{% if "nsec3-dynamic.kasp" in zones %} zone "nsec3-dynamic.kasp" { type primary; file "nsec3-dynamic.kasp.db"; dnssec-policy "nsec3"; allow-update { any; }; }; +{% endif %}{# nsec3-dynamic.kasp #} +{% if "nsec3-other.kasp" in zones %} /* This zone uses non-default NSEC3 settings. */ zone "nsec3-other.kasp" { type primary; file "nsec3-other.kasp.db"; dnssec-policy "nsec3-other"; }; +{% endif %}{# nsec3-other.kasp #} +{% if "nsec3-change.kasp" in zones %} /* These zones will be reconfigured to use other NSEC3 settings. */ zone "nsec3-change.kasp" { type primary; file "nsec3-change.kasp.db"; dnssec-policy "@nsec3_change@"; }; +{% endif %}{# nsec3-change.kasp #} +{% if "nsec3-dynamic-change.kasp" in zones %} zone "nsec3-dynamic-change.kasp" { type primary; file "nsec3-dynamic-change.kasp.db"; @@ -60,28 +71,36 @@ zone "nsec3-dynamic-change.kasp" { dnssec-policy "@nsec3_change@"; allow-update { any; }; }; +{% endif %}{# nsec3-dynamic-change.kasp #} +{% if "nsec3-to-optout.kasp" in zones %} /* The zone will be reconfigured to use opt-out. */ zone "nsec3-to-optout.kasp" { type primary; file "nsec3-to-optout.kasp.db"; dnssec-policy "@nsec3_to_optout@"; }; +{% endif %}{# nsec3-to-optout.kasp #} +{% if "nsec3-from-optout.kasp" in zones %} /* The zone will be reconfigured to disable opt-out. */ zone "nsec3-from-optout.kasp" { type primary; file "nsec3-from-optout.kasp.db"; dnssec-policy "@nsec3_from_optout@"; }; +{% endif %}{# nsec3-from-optout.kasp #} +{% if "nsec3-to-nsec.kasp" in zones %} /* The zone starts with NSEC3, but will be reconfigured to use NSEC. */ zone "nsec3-to-nsec.kasp" { type primary; file "nsec3-to-nsec.kasp.db"; dnssec-policy "@nsec3_to_nsec@"; }; +{% endif %}{# nsec3-to-nsec.kasp #} +{% if "nsec3-fails-to-load.kasp" in zones %} /* * The zone fails to load, this should not prevent shutdown. * The zone is fixed after a reconfig. @@ -92,7 +111,9 @@ zone "nsec3-fails-to-load.kasp" { dnssec-policy "nsec3"; allow-update { any; }; }; +{% endif %}{# nsec3-fails-to-load.kasp #} +{% if "nsec3-dynamic-to-inline.kasp" in zones %} /* These zones switch from dynamic to inline-signing or vice versa. */ zone "nsec3-dynamic-to-inline.kasp" { type primary; @@ -101,9 +122,11 @@ zone "nsec3-dynamic-to-inline.kasp" { {% if not reconfiged %} allow-update { any; }; inline-signing no; -{% endif %} +{% endif %}{# not reconfiged #} }; +{% endif %}{# nsec3-dynamic-to-inline.kasp #} +{% if "nsec3-inline-to-dynamic.kasp" in zones %} zone "nsec3-inline-to-dynamic.kasp" { type primary; file "nsec3-inline-to-dynamic.kasp.db"; @@ -111,34 +134,11 @@ zone "nsec3-inline-to-dynamic.kasp" { {% if reconfiged %} allow-update { any; }; inline-signing no; -{% endif %} - +{% endif %}{# reconfiged #} }; +{% endif %}{# nsec3-inline-to-dynamic.kasp #} -{% if not reconfiged %} - -/* - * Test adding a NSEC3 record to an inline-signing dnssec-policy zone. - */ -zone "nsec3-dynamic-update-inline.kasp" { - type primary; - file "nsec3-dynamic-update-inline.kasp.db"; - allow-update { any; }; - dnssec-policy "nsec"; -}; - -/* - * This zone will have an empty nonterminal node added and a node deleted. - */ -zone "nsec3-xfr-inline.kasp" { - type secondary; - file "nsec3-xfr-inline.kasp.db"; - dnssec-policy "nsec"; - primaries { 10.53.0.2; }; -}; - -{% else %} - +{% if "nsec3-ent.kasp" in zones %} /* * This zone will have an empty nonterminal node added and a node deleted. */ @@ -148,5 +148,32 @@ zone "nsec3-ent.kasp" { dnssec-policy "nsec3"; inline-signing yes; }; +{% endif %}{# nsec3-ent.kasp #} -{% endif %} +{% if not reconfiged %} + +{% if "nsec3-dynamic-update-inline.kasp" in zones %} +/* + * Test adding a NSEC3 record to an inline-signing dnssec-policy zone. + */ +zone "nsec3-dynamic-update-inline.kasp" { + type primary; + file "nsec3-dynamic-update-inline.kasp.db"; + allow-update { any; }; + dnssec-policy "nsec"; +}; +{% endif %}{# nsec3-dynamic-update-inline.kasp #} + +{% if "nsec3-xfr-inline.kasp" in zones %} +/* + * This zone will have an empty nonterminal node added and a node deleted. + */ +zone "nsec3-xfr-inline.kasp" { + type secondary; + file "nsec3-xfr-inline.kasp.db"; + dnssec-policy "nsec"; + primaries { 10.53.0.2; }; +}; +{% endif %}{# nsec3-xfr-inline.kasp #} + +{% endif %}{# not reconfiged #} diff --git a/bin/tests/system/nsec3/ns3/named-rsasha1.conf.j2 b/bin/tests/system/nsec3/ns3/named-rsasha1.conf.j2 index c2cbf485ce..7c47ae2d54 100644 --- a/bin/tests/system/nsec3/ns3/named-rsasha1.conf.j2 +++ b/bin/tests/system/nsec3/ns3/named-rsasha1.conf.j2 @@ -21,6 +21,8 @@ dnssec-policy "rsasha1" { }; }; + +{% if "rsasha1-to-nsec3.kasp" in zones %} /* * This zone starts with NSEC, but will be reconfigured to use NSEC3. * This should work despite the incompatible RSAHSHA1 algorithm, @@ -31,7 +33,9 @@ zone "rsasha1-to-nsec3.kasp" { file "rsasha1-to-nsec3.kasp.db"; dnssec-policy "@rsasha1_to_nsec3@"; }; +{% endif %}{# rsasha1-to-nsec3.kasp #} +{% if "rsasha1-to-nsec3-wait.kasp" in zones %} /* * This zone starts with NSEC, but will be reconfigured to use NSEC3. * This should block because RSASHA1 is not compatible with NSEC3, @@ -42,7 +46,9 @@ zone "rsasha1-to-nsec3-wait.kasp" { file "rsasha1-to-nsec3-wait.kasp.db"; dnssec-policy "@rsasha1_to_nsec3@"; }; +{% endif %}{# rsasha1-to-nsec3-wait.kasp #} +{% if "nsec3-to-rsasha1.kasp" in zones %} /* * This zone starts with NSEC3, but will be reconfigured to use NSEC with an * NSEC only algorithm. This should work despite the incompatible RSAHSHA1 @@ -53,7 +59,9 @@ zone "nsec3-to-rsasha1.kasp" { file "nsec3-to-rsasha1.kasp.db"; dnssec-policy "@nsec3_to_rsasha1@"; }; +{% endif %}{# nsec3-to-rsasha1.kasp #} +{% if "nsec3-to-rsasha1-ds.kasp" in zones %} /* * This zone starts with NSEC3, but will be reconfigured to use NSEC with an * NSEC only algorithm. This should also be fine because we are allowed @@ -65,3 +73,4 @@ zone "nsec3-to-rsasha1-ds.kasp" { file "nsec3-to-rsasha1-ds.kasp.db"; dnssec-policy "@nsec3_to_rsasha1@"; }; +{% endif %}{# nsec3-to-rsasha1-ds.kasp #} diff --git a/bin/tests/system/nsec3/tests_nsec3_change.py b/bin/tests/system/nsec3/tests_nsec3_change.py index 39b3a1cbfe..a65a1789be 100644 --- a/bin/tests/system/nsec3/tests_nsec3_change.py +++ b/bin/tests/system/nsec3/tests_nsec3_change.py @@ -31,6 +31,18 @@ from nsec3.common import ( ) +# include the following zones when rendering named configs +ZONES = { + "nsec3-change.kasp", +} + + +def bootstrap(): + return { + "zones": ZONES, + } + + @pytest.fixture(scope="module", autouse=True) def after_servers_start(ns3, templates): @@ -58,8 +70,12 @@ def after_servers_start(ns3, templates): # After reconfig, the NSEC3PARAM TTL should match the new SOA MINIMUM. # Reconfigure. - templates.render(f"{nsdir}/named-fips.conf", {"reconfiged": True}) - templates.render(f"{nsdir}/named-rsasha1.conf", {"reconfiged": True}) + data = { + "reconfiged": True, + "zones": ZONES, + } + templates.render(f"{nsdir}/named-fips.conf", data) + templates.render(f"{nsdir}/named-rsasha1.conf", data) # Wait for the NSEC3 chain is finished rebuilding. messages = [ diff --git a/bin/tests/system/nsec3/tests_nsec3_initial.py b/bin/tests/system/nsec3/tests_nsec3_initial.py index a8f52350fe..6ef80343a9 100644 --- a/bin/tests/system/nsec3/tests_nsec3_initial.py +++ b/bin/tests/system/nsec3/tests_nsec3_initial.py @@ -11,6 +11,8 @@ # pylint: disable=redefined-outer-name,unused-import +import os + import dns.update import pytest @@ -27,6 +29,40 @@ from nsec3.common import ( ) +# include the following zones when rendering named configs +ZONES = { + "nsec-to-nsec3.kasp", + "nsec3-xfr-inline.kasp", + "nsec3-dynamic-update-inline.kasp", + "nsec3.kasp", + "nsec3-dynamic.kasp", + "nsec3-change.kasp", + "nsec3-dynamic-change.kasp", + "nsec3-dynamic-to-inline.kasp", + "nsec3-inline-to-dynamic.kasp", + "nsec3-to-nsec.kasp", + "nsec3-to-optout.kasp", + "nsec3-from-optout.kasp", + "nsec3-other.kasp", +} + +if os.environ["RSASHA1_SUPPORTED"] == "1": + ZONES.update( + { + "rsasha1-to-nsec3.kasp", + "rsasha1-to-nsec3-wait.kasp", + "nsec3-to-rsasha1.kasp", + "nsec3-to-rsasha1-ds.kasp", + } + ) + + +def bootstrap(): + return { + "zones": ZONES, + } + + @pytest.mark.parametrize( "params", [ diff --git a/bin/tests/system/nsec3/tests_nsec3_reconfig.py b/bin/tests/system/nsec3/tests_nsec3_reconfig.py index 1f7ffec73e..666ba320b4 100644 --- a/bin/tests/system/nsec3/tests_nsec3_reconfig.py +++ b/bin/tests/system/nsec3/tests_nsec3_reconfig.py @@ -31,6 +31,38 @@ from nsec3.common import ( ) +# include the following zones when rendering named configs +ZONES = { + "nsec3-to-nsec.kasp", + "nsec-to-nsec3.kasp", + "nsec3.kasp", + "nsec3-dynamic.kasp", + "nsec3-dynamic-change.kasp", + "nsec3-dynamic-to-inline.kasp", + "nsec3-inline-to-dynamic.kasp", + # "nsec3-to-optout.kasp", + # "nsec3-from-optout.kasp", + "nsec3-other.kasp", + "nsec3-ent.kasp", +} + +if os.environ["RSASHA1_SUPPORTED"] == "1": + ZONES.update( + { + "rsasha1-to-nsec3-wait.kasp", + "nsec3-to-rsasha1.kasp", + "nsec3-to-rsasha1-ds.kasp", + "rsasha1-to-nsec3.kasp", + } + ) + + +def bootstrap(): + return { + "zones": ZONES, + } + + @pytest.fixture(scope="module", autouse=True) def after_servers_start(ns3, templates): # Ensure rsasha1-to-nsec3-wait.kasp is fully signed prior to reconfig. @@ -41,8 +73,12 @@ def after_servers_start(ns3, templates): isctest.kasp.check_dnssec_verify(ns3, zone) # Reconfigure. - templates.render(f"{ns3.identifier}/named-fips.conf", {"reconfiged": True}) - templates.render(f"{ns3.identifier}/named-rsasha1.conf", {"reconfiged": True}) + data = { + "reconfiged": True, + "zones": ZONES, + } + templates.render(f"{ns3.identifier}/named-fips.conf", data) + templates.render(f"{ns3.identifier}/named-rsasha1.conf", data) ns3.reconfigure() diff --git a/bin/tests/system/nsec3/tests_nsec3_reload.py b/bin/tests/system/nsec3/tests_nsec3_reload.py index 0e5dd0cfea..b3fb860288 100644 --- a/bin/tests/system/nsec3/tests_nsec3_reload.py +++ b/bin/tests/system/nsec3/tests_nsec3_reload.py @@ -26,6 +26,18 @@ from nsec3.common import ( ) +# include the following zones when rendering named configs +ZONES = { + "nsec3-fails-to-load.kasp", +} + + +def bootstrap(): + return { + "zones": ZONES, + } + + def test_nsec3_case(ns3): # Get test parameters. params = { diff --git a/bin/tests/system/nsec3/tests_nsec3_restart.py b/bin/tests/system/nsec3/tests_nsec3_restart.py index cae21f4413..e1e76c4f98 100644 --- a/bin/tests/system/nsec3/tests_nsec3_restart.py +++ b/bin/tests/system/nsec3/tests_nsec3_restart.py @@ -29,6 +29,19 @@ from nsec3.common import ( ) +# include the following zones when rendering named configs +ZONES = { + "nsec3.kasp", + "nsec3-other.kasp", +} + + +def bootstrap(): + return { + "zones": ZONES, + } + + def perform_nsec3_tests(server, params): # Get test parameters. zone = params["zone"] From 8cfccb9eacac5764fd85e1007886f88875a5232c Mon Sep 17 00:00:00 2001 From: Matthijs Mekking Date: Fri, 21 Nov 2025 09:47:08 +0100 Subject: [PATCH 12/13] Wait until zones are signed before reconfig In order to make the tests run reliable, first make sure the key managing for all the zones has finished before doing running the reconfig command. --- bin/tests/system/nsec3/tests_nsec3_reconfig.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/bin/tests/system/nsec3/tests_nsec3_reconfig.py b/bin/tests/system/nsec3/tests_nsec3_reconfig.py index 666ba320b4..47cd77ecf2 100644 --- a/bin/tests/system/nsec3/tests_nsec3_reconfig.py +++ b/bin/tests/system/nsec3/tests_nsec3_reconfig.py @@ -65,6 +65,13 @@ def bootstrap(): @pytest.fixture(scope="module", autouse=True) def after_servers_start(ns3, templates): + # First make sure all zones are properly signed. Here we specifically need + # to wait until all zones have finished key management before we can + # reconfigure the server, because changing the DNSSEC policy relies on + # zones having finished applying their initial policy. + for zone in ZONES: + isctest.kasp.wait_keymgr_done(ns3, zone) + # 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" From ba211de91288ef56a4c44464307ffbc906b48fe3 Mon Sep 17 00:00:00 2001 From: Matthijs Mekking Date: Fri, 21 Nov 2025 09:49:18 +0100 Subject: [PATCH 13/13] Wait longer before keymgr is done To check if the key managing for a zone is done, we check a log message. On some machines, with many zones this may take some time. Increate the timeout to a minute. Note that most zones will continue their test case much faster, but with many zones there may be some straggling. --- bin/tests/system/isctest/kasp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/tests/system/isctest/kasp.py b/bin/tests/system/isctest/kasp.py index 0fac77ff45..604a4f8669 100644 --- a/bin/tests/system/isctest/kasp.py +++ b/bin/tests/system/isctest/kasp.py @@ -1609,5 +1609,5 @@ def wait_keymgr_done(server: NamedInstance, zone: str, reconfig: bool = False) - messages.append("received control channel command 'reconfig'") messages.append("apply_configuration") messages.append(f"keymgr: {zone} done") - with server.watch_log_from_start() as watcher: + with server.watch_log_from_start(timeout=60) as watcher: watcher.wait_for_sequence(messages)