import dns.rcode
import dns.rdataclass
import dns.rdatatype
+import dns.rdtypes.ANY.RRSIG
import dns.rrset
from isctest.hypothesis.strategies import dns_names
)
+def do_test_query(qname, qtype, server, named_port) -> dns.message.Message:
+ query = dns.message.make_query(qname, qtype, use_edns=True, want_dnssec=True)
+ response = isctest.query.tcp(query, server, named_port, timeout=TIMEOUT)
+ isctest.check.is_response_to(response, query)
+ assert response.rcode() in (dns.rcode.NOERROR, dns.rcode.NXDOMAIN)
+ return response
+
+
+def assume_nx_and_no_delegation(qname):
+ assume(qname not in ZONE.all_existing_names)
+
+ # name must not be under a delegation or DNAME:
+ # it would not work with resolver ns4
+ assume(
+ not isctest.name.is_related_to_any(
+ qname,
+ (dns.name.NameRelation.EQUAL, dns.name.NameRelation.SUBDOMAIN),
+ ZONE.reachable_delegations.union(ZONE.reachable_dnames),
+ )
+ )
+
+
def nsec3_covers(rrset: dns.rrset.RRset, hashed_name: dns.name.Name) -> bool:
"""
- Test if 'hashed_name' is covered by an NSEC3 record in 'rrset'.
+ Test if 'hashed_name' is covered by an NSEC3 record in 'rrset', i.e. the name does not exist.
"""
prev_name = rrset.name
for nsec3 in rrset:
+ assert nsec3.flags == 0, "opt-out not supported by test logic"
next_name = nsec3.next_name(SUFFIX)
# Single name case.
def check_nsec3_covers(name: dns.name.Name, response: dns.message.Message) -> None:
+ """Given name provably does not exist"""
name_is_covered = False
nhash = dns.dnssec.nsec3_hash(
@pytest.mark.parametrize(
"server", [pytest.param(AUTH, id="ns3"), pytest.param(RESOLVER, id="ns4")]
)
-@given(name=dns_names(suffix=SUFFIX))
-# @given(name=just(dns.name.from_text(f"\000.\001.{SUFFIX}")))
-# @given(name=just(dns.name.from_text(f"a.wild.{SUFFIX}")))
-def test_dnssec_nsec3_nxdomain(server, name: dns.name.Name, named_port: int) -> None:
- noqname_test(server, name, named_port)
+@given(qname=dns_names(suffix=SUFFIX))
+def test_nxdomain(server, qname: dns.name.Name, named_port: int) -> None:
+ """A real NXDOMAIN, no wildcards involved"""
+ assume_nx_and_no_delegation(qname)
+ wname = ZONE.source_of_synthesis(qname)
+ assume(wname not in ZONE.reachable_wildcards)
+
+ check_nxdomain(server, named_port, qname)
@pytest.mark.parametrize(
"server", [pytest.param(AUTH, id="ns3"), pytest.param(RESOLVER, id="ns4")]
)
-@given(name=dns_names(suffix=ZONE.reachable.union(ZONE.ents)))
-def test_dnssec_nsec3_subdomain_nxdomain(
- server, name: dns.name.Name, named_port: int
-) -> None:
- noqname_test(server, name, named_port)
+@given(qname=dns_names(suffix=ZONE.ents))
+def test_ents(server, qname: dns.name.Name, named_port: int) -> None:
+ """ENT can have a wildcard under it"""
+ assume_nx_and_no_delegation(qname)
+ wname = ZONE.source_of_synthesis(qname)
+ # does qname match a wildcard under ENT?
+ if wname in ZONE.reachable_wildcards:
+ check_wildcard_synthesis(server, named_port, qname)
+ else:
+ check_nxdomain(server, named_port, qname)
-def noqname_test(server, name: dns.name.Name, named_port: int) -> None:
- # randomly generated name must not exist
- assume(name not in (ZONE.all_existing_names))
- # name must not be under a delegation or DNAME:
- # it would not work with resolver ns4
- assume(
- not isctest.name.is_related_to_any(
- name,
- (dns.name.NameRelation.EQUAL, dns.name.NameRelation.SUBDOMAIN),
- ZONE.reachable_delegations.union(ZONE.reachable_dnames),
- )
- )
+@pytest.mark.parametrize(
+ "server", [pytest.param(AUTH, id="ns3"), pytest.param(RESOLVER, id="ns4")]
+)
+@given(qname=dns_names(suffix=ZONE.reachable_wildcard_parents))
+def test_wildcard_synthesis(server, qname: dns.name.Name, named_port: int) -> None:
+ assume(qname not in ZONE.all_existing_names)
+
+ wname = ZONE.source_of_synthesis(qname)
+ assume(wname in ZONE.reachable_wildcards)
- query = dns.message.make_query(
- name, dns.rdatatype.A, use_edns=True, want_dnssec=True
+ check_wildcard_synthesis(server, named_port, qname)
+
+
+@pytest.mark.parametrize(
+ "server", [pytest.param(AUTH, id="ns3"), pytest.param(RESOLVER, id="ns4")]
+)
+@given(qname=dns_names(suffix=ZONE.reachable_wildcard_parents))
+def test_wildcard_nodata(server, qname: dns.name.Name, named_port: int) -> None:
+ assume(qname not in ZONE.all_existing_names)
+
+ wname = ZONE.source_of_synthesis(qname)
+ assume(wname in ZONE.reachable_wildcards)
+
+ check_wildcard_nodata(server, named_port, qname)
+
+
+def check_nsec3_owner(owner: dns.name.Name, response):
+ """Check response has NSEC3 RR matching given owner name, i.e. the name exists."""
+ name_hash = dns.dnssec.nsec3_hash(
+ owner, salt=None, iterations=0, algorithm=NSEC3Hash.SHA1
)
- response = isctest.query.tcp(query, server, named_port, timeout=TIMEOUT)
- isctest.check.is_response_to(response, query)
- assert response.rcode() in (dns.rcode.NOERROR, dns.rcode.NXDOMAIN)
+ nsec3_owner = dns.name.from_text(name_hash, SUFFIX)
+
+ nsec3_found = False
+ for rrset in response.authority:
+ if rrset.match(
+ nsec3_owner, dns.rdataclass.IN, dns.rdatatype.NSEC3, dns.rdatatype.NONE
+ ):
+ nsec3_found = True
+ assert (
+ nsec3_found
+ ), f"Expected matching NSEC3 for {owner} (hash={name_hash}) not found:\n{response}"
+
- ce, nce = ZONE.closest_encloser(name)
- # Response has NSEC3 that covers the next closer name
+def check_wildcard_nodata(server, named_port: int, qname: dns.name.Name) -> None:
+ response = do_test_query(qname, dns.rdatatype.AAAA, server, named_port)
+ assert response.rcode() is dns.rcode.NOERROR
+
+ ce, nce = ZONE.closest_encloser(qname)
+ check_nsec3_owner(ce, response)
check_nsec3_covers(nce, response)
- wname = ZONE.source_of_synthesis(name)
- if wname in ZONE.reachable_wildcards:
- wname_parent = dns.name.Name(wname[1:])
- assert name.is_subdomain(wname_parent)
- # expecting wildcard response with a signed A RRset
- assert response.rcode() is dns.rcode.NOERROR
- answer_sig = response.get_rrset(
- section="ANSWER",
- name=name,
- rdclass=dns.rdataclass.IN,
- rdtype=dns.rdatatype.RRSIG,
- covers=dns.rdatatype.A,
- )
- assert answer_sig is not None
- assert len(answer_sig) == 1
- # RRSIG labels field, RFC 4034 section 3.1.3 does not count:
- # - root label
- # - leftmost * label
- wildcard_parent_labels = answer_sig[0].labels + 1 # add root but not leftmost *
- assert wildcard_parent_labels < len(name)
- # ce should be wildcard name w/o wildcard label, nce one label longer
- assert ce == name.split(wildcard_parent_labels)[1]
- assert nce == name.split(wildcard_parent_labels + 1)[1]
- else:
- # no wildcard synthesis -> NXDOMAIN
- assert response.rcode() is dns.rcode.NXDOMAIN
- # Response must have closest encloser NSEC3
- ce_hash = dns.dnssec.nsec3_hash(
- ce, salt=None, iterations=0, algorithm=NSEC3Hash.SHA1
- )
- ce_nsec3 = dns.name.from_text(ce_hash, SUFFIX)
-
- ce_nsec3_match = False
- for rrset in response.authority:
- if rrset.match(
- ce_nsec3, dns.rdataclass.IN, dns.rdatatype.NSEC3, dns.rdatatype.NONE
- ):
- ce_nsec3_match = True
- assert (
- ce_nsec3_match
- ), f"Expected matching NSEC3 for {ce} (hash={ce_hash}) not found:\n {response}"
-
- # Response has NSEC3 that covers the wildcard
- check_nsec3_covers(wname, response)
+ wname = ZONE.source_of_synthesis(qname)
+ # expecting proof that wildcard owner does not have rdatatype requested
+ check_nsec3_owner(wname, response)
+
+
+def check_nxdomain(server, named_port: int, qname: dns.name.Name) -> None:
+ response = do_test_query(qname, dns.rdatatype.A, server, named_port)
+ assert response.rcode() is dns.rcode.NXDOMAIN
+
+ ce, nce = ZONE.closest_encloser(qname)
+ check_nsec3_owner(ce, response)
+ check_nsec3_covers(nce, response)
+
+ wname = ZONE.source_of_synthesis(qname)
+ check_nsec3_covers(wname, response)
+
+
+def check_wildcard_synthesis(server, named_port: int, qname: dns.name.Name) -> None:
+ """Expect wildcard response with a signed A RRset"""
+ response = do_test_query(qname, dns.rdatatype.A, server, named_port)
+ assert response.rcode() is dns.rcode.NOERROR
+
+ answer_sig = response.get_rrset(
+ section="ANSWER",
+ name=qname,
+ rdclass=dns.rdataclass.IN,
+ rdtype=dns.rdatatype.RRSIG,
+ covers=dns.rdatatype.A,
+ )
+ assert answer_sig is not None
+ assert len(answer_sig) == 1
+ rrsig = answer_sig[0]
+ assert isinstance(rrsig, dns.rdtypes.ANY.RRSIG.RRSIG)
+ # RRSIG labels field RFC 4034 section 3.1.3 does not count:
+ # - root label
+ # - leftmost * label
+ wildcard_parent_labels = rrsig.labels + 1 # add root but not leftmost *
+ assert wildcard_parent_labels < len(qname)
+
+ # 1. We have RRSIG from the wildcard '*.something', which proves the node
+ # 'something' exists (by definition - it has a child, so it exists, but
+ # maybe it is an ENT). Thus we expect closest encloser = 'something'
+ # 2. If wildcard synthesis is legitimate, QNAME itself and no nodes between
+ # QNAME and the closest encloser can exist. Because of DNS node existence
+ # rules it's sufficient to prove non-existence of next-closer name, i.e.
+ # <one_label_under>.<closest_encloser>, to deny existence of the whole
+ # subtree down to QNAME.
+
+ ce, nce = ZONE.closest_encloser(qname)
+ assert ce == qname.split(wildcard_parent_labels)[1]
+ # ce is proven to exist by the RRSIG
+ assert nce == qname.split(wildcard_parent_labels + 1)[1]
+ # nce must be proven to NOT exist
+ check_nsec3_covers(nce, response)