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 <<END || ret=1
-server 10.53.0.3 ${PORT}
-zone ${ZONE}.
-update add 04O18462RI5903H8RDVL0QDT5B528DUJ.${ZONE}. 3600 NSEC3 0 0 0 408A4B2D412A4E95 1JMDDPMTFF8QQLIOINSIG4CR9OTICAOC A RRSIG
-send
-END
-wait_for_log 10 "updating zone '${ZONE}/IN': update failed: explicit NSEC3 updates are not allowed in secure zones (REFUSED)" ns3/named.run || ret=1
-check_nsec
-
# Reconfig named.
ret=0
echo_i "reconfig dnssec-policy to trigger nsec3 rollovers"
--- /dev/null
+# 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 shutil
+import os
+
+from datetime import timedelta
+
+import dns
+import dns.update
+import pytest
+
+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-fips.conf",
+ ]
+)
+
+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",
+ [
+ pytest.param(
+ {
+ "zone": "nsec-to-nsec3.kasp",
+ "policy": "nsec",
+ "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": "rsasha1",
+ "key-properties": [
+ f"csk 0 {RSASHA1.number} 2048 goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden",
+ ],
+ },
+ id="rsasha1-to-nsec3.kasp",
+ marks=isctest.mark.with_algorithm("RSASHA1"),
+ ),
+ pytest.param(
+ {
+ "zone": "rsasha1-to-nsec3-wait.kasp",
+ "policy": "rsasha1",
+ "key-properties": [
+ f"csk 0 {RSASHA1.number} 2048 goal:omnipresent dnskey:omnipresent krrsig:omnipresent zrrsig:omnipresent ds:omnipresent",
+ ],
+ },
+ id="rsasha1-to-nsec3-wait.kasp",
+ marks=isctest.mark.with_algorithm("RSASHA1"),
+ ),
+ pytest.param(
+ {
+ # This is a secondary zone, where the primary is signed with
+ # NSEC3 but the dnssec-policy dictates NSEC.
+ "zone": "nsec3-xfr-inline.kasp",
+ "policy": "nsec",
+ "key-properties": [
+ f"csk 0 {ALGORITHM} {SIZE} goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden",
+ ],
+ "external-keys": [
+ f"csk 0 {ALGORITHM} {SIZE}",
+ ],
+ "external-keydir": "ns2",
+ },
+ id="nsec3-xfr-inline.kasp",
+ ),
+ pytest.param(
+ {
+ "zone": "nsec3-dynamic-update-inline.kasp",
+ "policy": "nsec",
+ "key-properties": [
+ f"csk 0 {ALGORITHM} {SIZE} goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden",
+ ],
+ },
+ id="nsec3-dynamic-update-inline.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)
+
+ # 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)
+
+ # Extra test for nsec3-dynamic-update-inline.kasp.
+ if zone == "nsec3-dynamic-update-inline.kasp":
+ isctest.log.info(f"dynamic update dnssec-policy zone {zone} with NSEC3")
+ update_msg = dns.update.UpdateMessage(zone)
+ update_msg.add(
+ f"04O18462RI5903H8RDVL0QDT5B528DUJ.{zone}.",
+ 3600,
+ "NSEC3",
+ "0 0 0 408A4B2D412A4E95 1JMDDPMTFF8QQLIOINSIG4CR9OTICAOC A RRSIG",
+ )
+
+ with ns3.watch_log_from_here() as watcher:
+ ns3.nsupdate(update_msg, expected_rcode=dns.rcode.REFUSED)
+ watcher.wait_for_line(
+ f"updating zone '{zone}/IN': update failed: explicit NSEC3 updates are not allowed in secure zones (REFUSED)"
+ )
+
+
+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
+
+
+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",
+ [
+ pytest.param(
+ {
+ "zone": "nsec3-to-rsasha1.kasp",
+ "policy": "nsec3",
+ "key-properties": [
+ f"csk 0 {ALGORITHM} {SIZE} 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": "nsec3",
+ "key-properties": [
+ f"csk 0 {ALGORITHM} {SIZE} goal:omnipresent dnskey:omnipresent krrsig:omnipresent zrrsig:omnipresent ds:omnipresent",
+ ],
+ },
+ id="nsec3-to-rsasha1-ds.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",
+ "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",
+ "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",
+ ),
+ pytest.param(
+ {
+ "zone": "nsec3-to-nsec.kasp",
+ "policy": "nsec3",
+ "key-properties": [
+ f"csk 0 {ALGORITHM} {SIZE} goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden",
+ ],
+ },
+ id="nsec3-to-nsec.kasp",
+ ),
+ pytest.param(
+ {
+ "zone": "nsec3-to-optout.kasp",
+ "policy": "nsec3",
+ "key-properties": [
+ f"csk 0 {ALGORITHM} {SIZE} goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden",
+ ],
+ },
+ id="nsec3-to-optout.kasp",
+ ),
+ pytest.param(
+ {
+ "zone": "nsec3-from-optout.kasp",
+ "policy": "optout",
+ "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-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["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
+
+ 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)
+
+ # 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.