Add dnssec_py/tests_sibling_ds: reject DS for sibling zones in referrals

Add a system test that verifies the resolver rejects DS records whose
owner name does not match the delegation (NS) name in a referral
response.

A custom authoritative server (ans4) serves the parent zone sibling-ds.
from zone file with delegations for child and sibling subzones.  Its
DomainHandler injects a DS record for sibling.sibling-ds into referrals
for child.sibling-ds.  The resolver must detect the mismatch, log "DS
doesn't match referral (NS)", and return SERVFAIL.

Assisted-by: Claude:claude-opus-4-8
This commit is contained in:
Nicki Křížek 2026-04-13 12:55:54 +00:00 committed by Mark Andrews
parent 95a268f119
commit a2b9dcff54
3 changed files with 137 additions and 0 deletions

View file

@ -0,0 +1,65 @@
# 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.
"""
Custom authoritative server for the sibling-ds test.
When returning a referral for child.sibling-ds, this server injects a DS
record for sibling.sibling-ds into the authority section. The resolver
should reject this because the DS owner name does not match the
delegation (NS) name.
"""
from collections.abc import AsyncGenerator
import dns.rdatatype
import dns.rrset
from isctest.asyncserver import (
AsyncDnsServer,
DnsResponseSend,
DomainHandler,
QueryContext,
ResponseAction,
)
class SiblingDsInjectionHandler(DomainHandler):
"""Inject a DS record for sibling.sibling-ds into child.sibling-ds referrals."""
domains = ["child.sibling-ds."]
async def get_responses(
self, qctx: QueryContext
) -> AsyncGenerator[ResponseAction, None]:
# The default zone-data response already has the NS delegation for
# child.sibling-ds. and glue. Add a DS record for the *sibling* zone
# (wrong name for this referral).
sibling_ds = dns.rrset.from_text(
"sibling.sibling-ds.",
300,
qctx.qclass,
dns.rdatatype.DS,
"12345 8 2 "
"49FD46E6C4B45C55D4AC69CBD3CD34AC1AFE51DE7B2B585ABCDEABCDEABCDEAB",
)
qctx.response.authority.append(sibling_ds)
yield DnsResponseSend(qctx.response)
def main() -> None:
server = AsyncDnsServer()
server.install_response_handler(SiblingDsInjectionHandler())
server.run()
if __name__ == "__main__":
main()

View file

@ -13,6 +13,8 @@ import pytest
DNSSEC_PY_MARK = pytest.mark.extra_artifacts(
[
"ans*/*.db",
"ans*/*.run",
"ns*/dsset-*",
"ns*/trusted.conf",
"ns*/zones/*.db",

View file

@ -0,0 +1,70 @@
# 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.
"""
Test that the resolver rejects DS records for sibling zones in referrals.
A custom authoritative server (ans4) returns a referral for
child.sibling-ds that includes a DS record for sibling.sibling-ds. The
resolver must detect that the DS owner does not match the delegation NS
name and treat the response as a form error.
"""
from re import compile as Re
from dnssec_py.common import DNSSEC_PY_MARK
from isctest.template import NS2, Nameserver, zones
from isctest.zone import Zone, configure_root
import isctest
pytestmark = DNSSEC_PY_MARK
ANS4 = Nameserver("ans4")
def bootstrap():
# Child zone on ns2 — the test queries a.child.sibling-ds which
# resolves to the default template A record (10.0.0.1).
child = Zone("child.sibling-ds", NS2)
child.configure()
# Sibling zone on ns2 — exists so the sibling DS in the referral
# refers to a real delegation.
sibling = Zone("sibling.sibling-ds", NS2)
sibling.configure()
# Parent zone rendered into ans4/ (subdir=None puts the .db file
# directly in the ans4 directory where AsyncDnsServer loads it).
parent = Zone("sibling-ds", ANS4, subdir=None)
parent.delegations = [child, sibling]
parent.configure()
# Root zone delegates sibling-ds. to ans4.
root = configure_root([parent])
return {
"trust_anchors": root.trust_anchors(),
"zones": zones([root, child, sibling]),
}
def test_sibling_ds_rejected(ns9):
"""Resolver must reject a referral that contains DS for a sibling zone."""
log_ds_mismatch = Re(r"DS doesn't match the delegation owner name")
msg = isctest.query.create("a.child.sibling-ds.", "A")
with ns9.watch_log_from_here() as watcher:
res = isctest.query.tcp(msg, ns9.ip)
watcher.wait_for_line(log_ds_mismatch)
isctest.check.servfail(res)