From: Matthijs Mekking Date: Tue, 30 Sep 2025 11:25:22 +0000 (+0200) Subject: Rewrite nsec3 system test to pytest (2/4) X-Git-Tag: v9.21.16~38^2~8 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=2c7190609b51371b85237419650ecfea8e51d26e;p=thirdparty%2Fbind9.git 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. --- diff --git a/bin/tests/system/nsec3/tests.sh b/bin/tests/system/nsec3/tests.sh index 0414fb15f9a..9a4a2d22f7d 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 6f5cfcdafce..99247f89735 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 00000000000..7ce5846ed3b --- /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