Add NSEC3 answer correctness test to dnssec_py

Rewrite nsec3_answer/tests_nsec3.py as dnssec_py/tests_nsec3_answer.py
using the isctest.zone helpers for zone setup. ns1 (auth) and ns2
(resolver) were renumbered to ns2 and ns9 respectively to fit the
existing dnssec_py server infrastructure.

Assisted-by: Claude:claude-opus-4-8
This commit is contained in:
Nicki Křížek 2026-06-08 15:26:02 +00:00
parent b8c1b645ea
commit ca890789cd
7 changed files with 88 additions and 156 deletions

View file

@ -0,0 +1,47 @@
; This zone file intentionally isn't a jinja2 template.
;
; It needs to be read before the templates are rended, to generate hypothesis
; test cases.
$ORIGIN nsec3-answer.
$TTL 300
nsec3-answer. IN SOA . . (
1 ; serial
20 ; refresh (20 seconds)
20 ; retry (20 seconds)
1814400 ; expire (3 weeks)
3600 ; minimum (1 hour)
)
nsec3-answer. NS ns2
ns2 A 10.53.0.2
02hc3em7bdd011a0gms3hkkjt2if5vp8 A 10.0.0.0
a A 10.0.0.1
*.a.a A 10.0.0.6
a.a.a.a A 10.0.0.3
b A 10.0.0.2
b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b A 10.0.0.2
cname CNAME does-not-exist
cname.cname CNAME cname
cname.ent.cname CNAME cname.cname
d A 10.0.0.4
dname-to-nowhere DNAME does-not-exist
; DNAME owner longer than target to avoid YXDOMAIN dependent on QNAME
insecure NS a.root-servers.nil
ns.insecure A 10.53.0.3
a.root-servers.nil A 10.53.0.1
secure NS a.root-servers.nil
secure DS 11111 13 255 00
occluded.secure A 0.0.0.0
*.wild A 10.0.0.6
explicit.wild A 192.0.2.66
z A 10.0.0.26
; randomly generated subtree to excercise unknown corner cases
; intentionally small, to not blow up algorithms with quadratic complexity in ZoneAnalyzer and name generator
a.a.a.b.a.a.a.b.a.a.b.b.a.random TXT "r"
b.b.a.a.b.b.a.a.a.b.b.a.b.a.a.a.a.a.b.a.a.b.a.b.a.b.b.b.b.b.a.a.a.a.b.a.a.a.b.a.a.b.b.a.random TXT "r"
a.a.a.b.b.a.b.b.a.b.a.b.a.b.a.b.b.b.a.random TXT "r"
b.b.a.b.a.b.a.a.a.b.a.a.b.a.a.a.a.b.b.a.b.b.a.b.a.b.a.b.a.b.b.b.a.random TXT "r"
a.b.a.a.b.a.b.a.b.a.a.b.a.b.a.a.a.b.b.a.b.b.a.a.b.b.a.a.b.a.b.a.b.b.b.b.a.a.a.a.a.a.a.a.b.a.b.a.b.b.a.b.a.b.a.a.a.b.a.a.b.a.a.a.a.b.b.a.b.b.a.b.a.b.a.b.a.b.b.b.a.random TXT "r"
a.a.a.a.a.b.b.a.a.b.a.a.b.a.a.b.b.a.a.a.b.a.a.a.b.b.b.b.b.a.a.a.b.b.b.b.b.b.a.b.b.b.a.a.b.b.b.b.a.a.a.a.b.a.b.b.a.b.a.a.b.b.b.b.b.b.b.a.b.b.a.b.a.b.a.a.a.b.b.a.a.b.b.a.b.a.b.b.a.b.b.b.a.b.b.b.b.b.a.a.b.a.a.a.b.b.a.a.a.b.b.b.b.b.a.random TXT "r"

View file

@ -1,5 +1,3 @@
#!/usr/bin/python3
# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
#
# SPDX-License-Identifier: MPL-2.0
@ -11,7 +9,7 @@
# See the COPYRIGHT file distributed with this work for additional
# information regarding copyright ownership.
# Silence incorrect warnings cause by hypothesis.assume()
# Silence incorrect warnings caused by hypothesis.assume()
# https://github.com/pylint-dev/pylint/issues/10785#issuecomment-3677224217
# pylint: disable=unreachable
@ -20,6 +18,7 @@ from dataclasses import dataclass
from pathlib import Path
import os
import time
from hypothesis import assume, given
@ -34,17 +33,43 @@ import dns.rdtypes.ANY.RRSIG
import dns.rrset
import pytest
from dnssec_py.common import DNSSEC_PY_MARK
from isctest.hypothesis.strategies import dns_names, sampled_from
from isctest.template import NS2, zones
from isctest.zone import Zone, configure_root
import isctest
import isctest.name
SUFFIX = dns.name.from_text(".")
AUTH = "10.53.0.1"
RESOLVER = "10.53.0.2"
pytestmark = DNSSEC_PY_MARK
def bootstrap():
zone = Zone(
"nsec3-answer",
NS2,
signed=True,
)
zone.add_keys()
salt = int(time.time()) // 3600 % 65536
salt_hex = f"{salt:04X}"
isctest.log.info(f"NSEC3 salt for this hour: {salt_hex}")
zone.sign(f"-3 {salt_hex}")
root = configure_root([zone])
return {
"trust_anchors": root.trust_anchors(),
"zones": zones([root, zone]),
}
SUFFIX = dns.name.from_text("nsec3-answer.")
AUTH = "10.53.0.2"
RESOLVER = "10.53.0.9"
TIMEOUT = 5
ZONE = isctest.name.ZoneAnalyzer.read_path(
Path(os.environ["srcdir"]) / "nsec3_answer/ns1/root.db.in", origin=SUFFIX
Path(os.environ["srcdir"]) / "dnssec_py/ns2/zones/nsec3-answer.db",
origin=SUFFIX,
)
@ -71,7 +96,7 @@ def do_test_query(
@pytest.mark.parametrize(
"server", [pytest.param(AUTH, id="ns1"), pytest.param(RESOLVER, id="ns2")]
"server", [pytest.param(AUTH, id="ns2"), pytest.param(RESOLVER, id="ns9")]
)
@given(
qname=sampled_from(
@ -84,7 +109,7 @@ def test_nodata(server: str, qname: dns.name.Name, named_port: int) -> None:
check_nodata(qname, nsec3check)
@pytest.mark.parametrize("server", [pytest.param(AUTH, id="ns1")])
@pytest.mark.parametrize("server", [pytest.param(AUTH, id="ns2")])
@given(
qname=dns_names(
suffix=(ZONE.delegations - ZONE.get_names_with_type(dns.rdatatype.DS))
@ -116,7 +141,7 @@ def assume_nx_and_no_delegation(qname: dns.name.Name) -> None:
assume(qname not in ZONE.all_existing_names)
# name must not be under a delegation or DNAME:
# it would not work with resolver ns2
# it would not work with resolver ns9
assume(
not is_related_to_any(
qname,
@ -127,7 +152,7 @@ def assume_nx_and_no_delegation(qname: dns.name.Name) -> None:
@pytest.mark.parametrize(
"server", [pytest.param(AUTH, id="ns1"), pytest.param(RESOLVER, id="ns2")]
"server", [pytest.param(AUTH, id="ns2"), pytest.param(RESOLVER, id="ns9")]
)
@given(qname=dns_names(suffix=SUFFIX))
def test_nxdomain(server: str, qname: dns.name.Name, named_port: int) -> None:
@ -141,7 +166,7 @@ def test_nxdomain(server: str, qname: dns.name.Name, named_port: int) -> None:
@pytest.mark.parametrize(
"server", [pytest.param(AUTH, id="ns1"), pytest.param(RESOLVER, id="ns2")]
"server", [pytest.param(AUTH, id="ns2"), pytest.param(RESOLVER, id="ns9")]
)
@given(qname=sampled_from(sorted(ZONE.get_names_with_type(dns.rdatatype.CNAME))))
def test_cname_nxdomain(server: str, qname: dns.name.Name, named_port: int) -> None:
@ -157,7 +182,7 @@ def test_cname_nxdomain(server: str, qname: dns.name.Name, named_port: int) -> N
@pytest.mark.parametrize(
"server", [pytest.param(AUTH, id="ns1"), pytest.param(RESOLVER, id="ns2")]
"server", [pytest.param(AUTH, id="ns2"), pytest.param(RESOLVER, id="ns9")]
)
@given(qname=dns_names(suffix=ZONE.get_names_with_type(dns.rdatatype.DNAME)))
def test_dname_nxdomain(server: str, qname: dns.name.Name, named_port: int) -> None:
@ -175,7 +200,7 @@ def test_dname_nxdomain(server: str, qname: dns.name.Name, named_port: int) -> N
@pytest.mark.parametrize(
"server", [pytest.param(AUTH, id="ns1"), pytest.param(RESOLVER, id="ns2")]
"server", [pytest.param(AUTH, id="ns2"), pytest.param(RESOLVER, id="ns9")]
)
@given(qname=dns_names(suffix=ZONE.ents))
def test_ents(server: str, qname: dns.name.Name, named_port: int) -> None:
@ -193,7 +218,7 @@ def test_ents(server: str, qname: dns.name.Name, named_port: int) -> None:
@pytest.mark.parametrize(
"server", [pytest.param(AUTH, id="ns1"), pytest.param(RESOLVER, id="ns2")]
"server", [pytest.param(AUTH, id="ns2"), pytest.param(RESOLVER, id="ns9")]
)
@given(qname=dns_names(suffix=ZONE.reachable_wildcard_parents))
def test_wildcard_synthesis(server: str, qname: dns.name.Name, named_port: int) -> None:
@ -207,7 +232,7 @@ def test_wildcard_synthesis(server: str, qname: dns.name.Name, named_port: int)
@pytest.mark.parametrize(
"server", [pytest.param(AUTH, id="ns1"), pytest.param(RESOLVER, id="ns2")]
"server", [pytest.param(AUTH, id="ns2"), pytest.param(RESOLVER, id="ns9")]
)
@given(qname=dns_names(suffix=ZONE.reachable_wildcard_parents))
def test_wildcard_nodata(server: str, qname: dns.name.Name, named_port: int) -> None:

View file

@ -1,18 +0,0 @@
// NS1
options {
query-source address 10.53.0.1;
notify-source 10.53.0.1;
transfer-source 10.53.0.1;
port @PORT@;
pid-file "named.pid";
listen-on { 10.53.0.1; };
listen-on-v6 { none; };
recursion no;
dnssec-validation no;
};
zone "." {
type primary;
file "root.db.signed";
};

View file

@ -1,40 +0,0 @@
$TTL 300
. IN SOA . . (
2025063000 ; serial
600 ; refresh
600 ; retry
1200 ; expire
600 ; minimum
)
. NS a.root-servers.nil.
02hc3em7bdd011a0gms3hkkjt2if5vp8. A 10.0.0.0
a. A 10.0.0.1
*.a.a. A 10.0.0.6
a.a.a.a. A 10.0.0.3
b. A 10.0.0.2
b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b. A 10.0.0.2
cname. CNAME does-not-exist.
cname.cname. CNAME cname.
cname.ent.cname. CNAME cname.cname.
d. A 10.0.0.4
dname-to-nowhere. DNAME does-not-exist.
; DNAME owner longer than target to avoid YXDOMAIN dependent on QNAME
insecure. NS a.root-servers.nil.
ns.insecure. A 10.53.0.3
a.root-servers.nil. A 10.53.0.1
secure. NS a.root-servers.nil.
secure. DS 11111 13 255 00
occluded.secure. A 0.0.0.0
*.wild. A 10.0.0.6
explicit.wild. A 192.0.2.66
z. A 10.0.0.26
; randomly generated subtree to excercise unknown corner cases
; intentionally small, to not blow up algorithms with quadratic complexity in ZoneAnalyzer and name generator
a.a.a.b.a.a.a.b.a.a.b.b.a.random. TXT "r"
b.b.a.a.b.b.a.a.a.b.b.a.b.a.a.a.a.a.b.a.a.b.a.b.a.b.b.b.b.b.a.a.a.a.b.a.a.a.b.a.a.b.b.a.random. TXT "r"
a.a.a.b.b.a.b.b.a.b.a.b.a.b.a.b.b.b.a.random. TXT "r"
b.b.a.b.a.b.a.a.a.b.a.a.b.a.a.a.a.b.b.a.b.b.a.b.a.b.a.b.a.b.b.b.a.random. TXT "r"
a.b.a.a.b.a.b.a.b.a.a.b.a.b.a.a.a.b.b.a.b.b.a.a.b.b.a.a.b.a.b.a.b.b.b.b.a.a.a.a.a.a.a.a.b.a.b.a.b.b.a.b.a.b.a.a.a.b.a.a.b.a.a.a.a.b.b.a.b.b.a.b.a.b.a.b.a.b.b.b.a.random. TXT "r"
a.a.a.a.a.b.b.a.a.a.a.a.b.b.a.a.b.a.a.b.a.a.b.b.a.a.a.b.a.a.a.b.b.b.b.b.a.a.a.b.b.b.b.b.b.a.b.b.b.a.a.b.b.b.b.a.a.a.a.b.a.b.b.a.b.a.a.b.b.b.b.b.b.b.a.b.b.a.b.a.b.a.a.a.b.b.a.a.b.b.a.b.a.b.b.a.b.b.b.a.b.b.b.b.b.a.a.b.a.a.a.b.b.a.a.a.b.b.b.b.b.a.random. TXT "r"

View file

@ -1,34 +0,0 @@
#!/bin/sh -e
# 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.
# shellcheck source=conf.sh
. ../../conf.sh
set -e
zone=.
infile=root.db.in
zonefile=root.db
echo_i "ns1/sign.sh"
ksk=$("$KEYGEN" -q -fk -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" "$zone")
zsk=$("$KEYGEN" -q -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" "$zone")
cat "$infile" "$ksk.key" "$zsk.key" >"$zonefile"
SALT="$(printf "%04x" "$(($(date +%s) / 3600 % 65536))")"
echo_ic "NSEC3 salt for this hour: $SALT"
"$SIGNER" -3 "$SALT" -o "$zone" "$zonefile" 2>&1 >"$zonefile.sign.log"
keyfile_to_initial_ds "$ksk" >managed-keys.conf

View file

@ -1,26 +0,0 @@
// validating resolver
options {
query-source address 10.53.0.2;
notify-source 10.53.0.2;
transfer-source 10.53.0.2;
port @PORT@;
pid-file "named.pid";
listen-on { 10.53.0.2; };
listen-on-v6 { none; };
recursion yes;
dnssec-validation yes;
};
controls {
inet 10.53.0.2 port @CONTROLPORT@ allow { any; } keys { rndc_key; };
};
include "../../_common/rndc.key";
zone "." {
type hint;
file "../../_common/root.hint";
};
include "../ns1/managed-keys.conf";

View file

@ -1,22 +0,0 @@
#!/bin/sh -e
# 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.
# shellcheck source=conf.sh
. ../conf.sh
set -e
(
cd ns1
$SHELL sign.sh
)