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.
This commit is contained in:
Matthijs Mekking 2025-09-30 13:25:22 +02:00
parent ba1ffe56e4
commit 2c7190609b
3 changed files with 355 additions and 190 deletions

View file

@ -235,164 +235,6 @@ key_clear "KEY2"
key_clear "KEY3"
key_clear "KEY4"
# Reconfig named.
ret=0
echo_i "reconfig dnssec-policy to trigger nsec3 rollovers"
if [ $RSASHA1_SUPPORTED = 0 ]; then
copy_setports ns3/named2-fips.conf.in ns3/named.conf
else
copy_setports ns3/named2-fips.conf.in ns3/named-fips.conf
# includes named-fips.conf
cp ns3/named2.conf.in ns3/named.conf
fi
rndc_reconfig ns3 10.53.0.3
# Zone: nsec-to-nsec3.kasp. (reconfigured)
set_zone_policy "nsec-to-nsec3.kasp" "nsec3" 1 3600
set_nsec3param "0" "0"
set_key_default_values "KEY1"
echo_i "check zone ${ZONE} after reconfig"
check_nsec3
if [ $RSASHA1_SUPPORTED = 1 ]; then
# Zone: rsasha1-to-nsec3.kasp.
set_zone_policy "rsasha1-to-nsec3.kasp" "nsec3" 2 3600
set_server "ns3" "10.53.0.3"
set_key_rsasha1_values "KEY1"
set_key_states "KEY1" "hidden" "unretentive" "unretentive" "unretentive" "hidden"
set_keysigning "KEY1" "no"
set_zonesigning "KEY1" "no"
set_key_default_values "KEY2"
echo_i "check zone ${ZONE} after reconfig"
check_nsec3
# Zone: rsasha1-to-nsec3-wait.kasp.
set_zone_policy "rsasha1-to-nsec3-wait.kasp" "nsec3" 2 3600
set_server "ns3" "10.53.0.3"
set_key_rsasha1_values "KEY1"
set_key_states "KEY1" "hidden" "omnipresent" "omnipresent" "omnipresent" "omnipresent"
set_key_default_values "KEY2"
echo_i "check zone ${ZONE} after reconfig"
check_nsec
# Zone: nsec3-to-rsasha1.kasp.
set_zone_policy "nsec3-to-rsasha1.kasp" "rsasha1" 2 3600
set_nsec3param "1" "0"
set_server "ns3" "10.53.0.3"
set_key_default_values "KEY1"
set_key_states "KEY1" "hidden" "unretentive" "unretentive" "unretentive" "hidden"
set_keysigning "KEY1" "no"
set_zonesigning "KEY1" "no"
set_key_rsasha1_values "KEY2"
echo_i "check zone ${ZONE} after reconfig"
check_nsec
# Zone: nsec3-to-rsasha1-ds.kasp.
set_zone_policy "nsec3-to-rsasha1-ds.kasp" "rsasha1" 2 3600
set_nsec3param "1" "0"
set_server "ns3" "10.53.0.3"
set_key_default_values "KEY1"
set_key_states "KEY1" "hidden" "omnipresent" "omnipresent" "omnipresent" "omnipresent"
set_key_rsasha1_values "KEY2"
echo_i "check zone ${ZONE} after reconfig"
check_nsec
key_clear "KEY1"
key_clear "KEY2"
fi
# Zone: nsec3.kasp. (same)
set_zone_policy "nsec3.kasp" "nsec3" 1 3600
set_nsec3param "0" "0"
set_key_default_values "KEY1"
echo_i "check zone ${ZONE} after reconfig"
check_nsec3
# Zone: nsec3-dyamic.kasp. (same)
set_zone_policy "nsec3-dynamic.kasp" "nsec3" 1 3600
set_nsec3param "0" "0"
set_key_default_values "KEY1"
echo_i "check zone ${ZONE} after reconfig"
check_nsec3
# Zone: nsec3-change.kasp. (reconfigured)
set_zone_policy "nsec3-change.kasp" "nsec3-other" 1 3600
set_nsec3param "1" "8"
set_key_default_values "KEY1"
echo_i "check zone ${ZONE} after reconfig"
check_nsec3
# Test that NSEC3PARAM TTL is equal to new SOA MINIMUM.
n=$((n + 1))
echo_i "check TTL of NSEC3PARAM in zone $ZONE is updated after SOA MINIMUM changed ($n)"
ret=0
# Check NSEC3PARAM TTL.
dig_with_opts +noquestion "@${SERVER}" "$ZONE" NSEC3PARAM >"dig.out.nsec3param.test$n" || ret=1
grep "${ZONE}\..*900.*IN.*NSEC3PARAM" "dig.out.nsec3param.test$n" >/dev/null || ret=1
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
# Using rndc signing -nsec3param (should fail)
echo_i "use rndc signing -nsec3param ${ZONE} to change NSEC3 settings"
rndccmd $SERVER signing -nsec3param 1 1 12 ffff $ZONE >rndc.signing.test$n.$ZONE || log_error "failed to call rndc signing -nsec3param $ZONE"
grep "zone uses dnssec-policy, use rndc dnssec command instead" rndc.signing.test$n.$ZONE >/dev/null || log_error "rndc signing -nsec3param should fail"
check_nsec3
# Zone: nsec3-dynamic-change.kasp. (reconfigured)
set_zone_policy "nsec3-dynamic-change.kasp" "nsec3-other" 1 3600
set_nsec3param "1" "8"
set_key_default_values "KEY1"
echo_i "check zone ${ZONE} after reconfig"
check_nsec3
# Zone: nsec3-dynamic-to-inline.kasp. (same)
set_zone_policy "nsec3-dynamic-to-inline.kasp" "nsec3" 1 3600
set_nsec3param "0" "0"
set_key_default_values "KEY1"
echo_i "check zone ${ZONE} after reconfig"
check_nsec3
# Zone: nsec3-inline-to-dynamic.kasp. (same)
set_zone_policy "nsec3-inline-to-dynamic.kasp" "nsec3" 1 3600
set_nsec3param "0" "0"
set_key_default_values "KEY1"
echo_i "initial check zone ${ZONE}"
check_nsec3
# Zone: nsec3-to-nsec.kasp. (reconfigured)
set_zone_policy "nsec3-to-nsec.kasp" "nsec" 1 3600
set_nsec3param "1" "8"
set_key_default_values "KEY1"
echo_i "check zone ${ZONE} after reconfig"
check_nsec
# Zone: nsec3-to-optout.kasp. (reconfigured)
# DISABLED:
# There is a bug in the nsec3param building code that thinks when the
# optout bit is changed, the chain already exists. [GL #2216]
#set_zone_policy "nsec3-to-optout.kasp" "optout" 1 3600
#set_nsec3param "1" "0"
#set_key_default_values "KEY1"
#echo_i "check zone ${ZONE} after reconfig"
#check_nsec3
# Zone: nsec3-from-optout.kasp. (reconfigured)
# DISABLED:
# There is a bug in the nsec3param building code that thinks when the
# optout bit is changed, the chain already exists. [GL #2216]
#set_zone_policy "nsec3-from-optout.kasp" "nsec3" 1 3600
#set_nsec3param "0" "0"
#set_key_default_values "KEY1"
#echo_i "check zone ${ZONE} after reconfig"
#check_nsec3
# Zone: nsec3-other.kasp. (same)
set_zone_policy "nsec3-other.kasp" "nsec3-other" 1 3600
set_nsec3param "1" "8"
set_key_default_values "KEY1"
echo_i "check zone ${ZONE} after reconfig"
check_nsec3
# Test NSEC3 and NSEC3PARAM is the same after restart
set_zone_policy "nsec3.kasp" "nsec3" 1 3600
set_nsec3param "0" "0"

View file

@ -11,8 +11,6 @@
# pylint: disable=redefined-outer-name,unused-import
import shutil
import dns.update
import pytest
@ -155,25 +153,6 @@ def test_nsec_case(ns3, params):
)
def wait_for_soa_update(server, fqdn):
verified = False
match = f"20 20 1814400 900"
for _ in range(5):
query = isctest.query.create(fqdn, dns.rdatatype.SOA)
response = isctest.query.tcp(query, server.ip, server.ports.dns, timeout=3)
for rrset in response.answer:
if match in rrset.to_text():
verified = True
if verified:
break
time.sleep(1)
return verified
@pytest.mark.parametrize(
"params",
[
@ -349,14 +328,3 @@ def test_nsec3_case(ns3, params):
response = isctest.query.tcp(query, ns3.ip)
assert response.rcode() == dns.rcode.NXDOMAIN
check_auth_nsec3(response, iterations, optout, salt)
# Extra test for nsec3-change.kasp.
if zone == "nsec3-change.kasp":
shutil.copyfile(
f"{ns3.identifier}/template2.db.in", f"{ns3.identifier}/{zone}.db"
)
ns3.rndc(f"reload {zone}")
wait_for_soa_update(ns3, fqdn)
# After reconfig, the NSEC3PARAM TTL should match the new SOA MINIMUM.

View file

@ -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