Open
fix(cve): 修复 CVE-2026-75029 在 bind 中的漏洞 #344
infra_teamAdd on Sep 17
fix(cve): 修复 CVE-2026-75029 在 bind 中的漏洞 #344
Open
Total: 38 files changed+24-8985
| @@ -1,69 +0,0 @@ | |||
| 1 | -From e3b23edcef81abe6b11e2fc5b6c0f656e0ce250a Mon Sep 17 00:00:00 2001 | ||
| 2 | -From: Evan Hunt <each@isc.org> | ||
| 3 | -Date: Thu, 21 May 2026 14:41:55 -0700 | ||
| 4 | -Subject: [PATCH] Check NSEC3 signer matches the owning zone | ||
| 5 | - | ||
| 6 | -When validating NSEC3 records, reject any signature whose signer field | ||
| 7 | -does not match the zone owning the NSEC3. | ||
| 8 | - | ||
| 9 | -This ensures that a child zone cannot impersonate its parent and forge | ||
| 10 | -NXDOMAIN responses for sibling domains. | ||
| 11 | - | ||
| 12 | -Fixes: isc-projects/bind9#5874 | ||
| 13 | -(cherry picked from commit 6e5066bb1f0f12d090e8707adb7d6ccf74f8012b) | ||
| 14 | - | ||
| 15 | -Conflict:no | ||
| 16 | -Reference:https://gitlab.isc.org/isc-projects/bind9/-/commit/e3b23edcef81abe6b11e2fc5b6c0f656e0ce250a | ||
| 17 | ---- | ||
| 18 | - lib/dns/dnssec.c | 19 +++++++++++++++++-- | ||
| 19 | - lib/isc/result.c | 2 +- | ||
| 20 | - 2 files changed, 18 insertions(+), 3 deletions(-) | ||
| 21 | - | ||
| 22 | -diff --git a/lib/dns/dnssec.c b/lib/dns/dnssec.c | ||
| 23 | -index 4b5ddc2184..712a7880fc 100644 | ||
| 24 | ---- a/lib/dns/dnssec.c | ||
| 25 | -+++ b/lib/dns/dnssec.c | ||
| 26 | -@@ -423,10 +423,25 @@ dns_dnssec_verify(const dns_name_t *name, dns_rdataset_t *set, dst_key_t *key, | ||
| 27 | - } | ||
| 28 | - | ||
| 29 | - /* | ||
| 30 | -- * NS, SOA and DNSKEY records are signed by their owner. | ||
| 31 | -- * DS records are signed by the parent. | ||
| 32 | -+ * NS, SOA and DNSKEY records are signed by their owners. | ||
| 33 | -+ * NSEC3 records are signed by the apex, exactly one level up | ||
| 34 | -+ * from their owner names. | ||
| 35 | -+ * DS records are signed by the parent zone. | ||
| 36 | - */ | ||
| 37 | - switch (set->type) { | ||
| 38 | -+ case dns_rdatatype_nsec3: { | ||
| 39 | -+ dns_name_t apex = DNS_NAME_INITEMPTY; | ||
| 40 | -+ labels = dns_name_countlabels(name); | ||
| 41 | -+ if (labels <= 1) { | ||
| 42 | -+ inc_stat(dns_dnssecstats_fail); | ||
| 43 | -+ return DNS_R_INVALIDNSEC3; | ||
| 44 | -+ } | ||
| 45 | -+ dns_name_split(name, labels - 1, NULL, &apex); | ||
| 46 | -+ if (!dns_name_equal(&apex, &sig.signer)) { | ||
| 47 | -+ inc_stat(dns_dnssecstats_fail); | ||
| 48 | -+ return DNS_R_SIGINVALID; | ||
| 49 | -+ } | ||
| 50 | -+ } break; | ||
| 51 | - case dns_rdatatype_ns: | ||
| 52 | - case dns_rdatatype_soa: | ||
| 53 | - case dns_rdatatype_dnskey: | ||
| 54 | -diff --git a/lib/isc/result.c b/lib/isc/result.c | ||
| 55 | -index 0c608deecd..86fdce5ef4 100644 | ||
| 56 | ---- a/lib/isc/result.c | ||
| 57 | -+++ b/lib/isc/result.c | ||
| 58 | -@@ -200,7 +200,7 @@ static const char *description[ISC_R_NRESULTS] = { | ||
| 59 | - [DNS_R_COVERINGNSEC] = "covering NSEC record returned", | ||
| 60 | - [DNS_R_MXISADDRESS] = "MX is an address", | ||
| 61 | - [DNS_R_DUPLICATE] = "duplicate query", | ||
| 62 | -- [DNS_R_INVALIDNSEC3] = "invalid NSEC3 owner name (wildcard)", | ||
| 63 | -+ [DNS_R_INVALIDNSEC3] = "invalid NSEC3 owner name", | ||
| 64 | - [DNS_R_NOTPRIMARY] = "not primary", | ||
| 65 | - [DNS_R_BROKENCHAIN] = "broken trust chain", | ||
| 66 | - [DNS_R_EXPIRED] = "expired", | ||
| 67 | --- | ||
| 68 | -2.43.0 | ||
| 69 | - | ||
| @@ -1,495 +0,0 @@ | |||
| 1 | -From 152bffd0d7ab03fdb096432a9274a2919be75fe0 Mon Sep 17 00:00:00 2001 | ||
| 2 | -From: Alessio Podda <alessio@isc.org> | ||
| 3 | -Date: Wed, 3 Jun 2026 10:42:43 +0200 | ||
| 4 | -Subject: [PATCH] Reproducer for #5874 NSEC3 impersonation | ||
| 5 | - | ||
| 6 | -LLM generated. | ||
| 7 | - | ||
| 8 | -(cherry picked from commit f3e2eb333be3ac636f745aa13cfb8d9ee8af87d8) | ||
| 9 | - | ||
| 10 | -Conflict:no | ||
| 11 | -Reference:https://gitlab.isc.org/isc-projects/bind9/-/commit/152bffd0d7ab03fdb096432a9274a2919be75fe0 | ||
| 12 | ---- | ||
| 13 | - .../repro_5874_nsec3_parent/ans1/ans.py | 18 ++ | ||
| 14 | - .../repro_5874_nsec3_parent/ns2/named.conf.j2 | 35 +++ | ||
| 15 | - .../system/repro_5874_nsec3_parent/server.py | 249 ++++++++++++++++++ | ||
| 16 | - .../tests_repro_5874_nsec3_parent.py | 144 ++++++++++ | ||
| 17 | - 4 files changed, 446 insertions(+) | ||
| 18 | - create mode 100644 bin/tests/system/repro_5874_nsec3_parent/ans1/ans.py | ||
| 19 | - create mode 100644 bin/tests/system/repro_5874_nsec3_parent/ns2/named.conf.j2 | ||
| 20 | - create mode 100644 bin/tests/system/repro_5874_nsec3_parent/server.py | ||
| 21 | - create mode 100644 bin/tests/system/repro_5874_nsec3_parent/tests_repro_5874_nsec3_parent.py | ||
| 22 | - | ||
| 23 | -diff --git a/bin/tests/system/repro_5874_nsec3_parent/ans1/ans.py b/bin/tests/system/repro_5874_nsec3_parent/ans1/ans.py | ||
| 24 | -new file mode 100644 | ||
| 25 | -index 0000000000..614f26a014 | ||
| 26 | ---- /dev/null | ||
| 27 | -+++ b/bin/tests/system/repro_5874_nsec3_parent/ans1/ans.py | ||
| 28 | -@@ -0,0 +1,18 @@ | ||
| 29 | -+#!/usr/bin/python3 | ||
| 30 | -+ | ||
| 31 | -+# Copyright (C) Internet Systems Consortium, Inc. ("ISC") | ||
| 32 | -+# | ||
| 33 | -+# SPDX-License-Identifier: MPL-2.0 | ||
| 34 | -+# | ||
| 35 | -+# This Source Code Form is subject to the terms of the Mozilla Public | ||
| 36 | -+# License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| 37 | -+# file, you can obtain one at https://mozilla.org/MPL/2.0/. | ||
| 38 | -+# | ||
| 39 | -+# See the COPYRIGHT file distributed with this work for additional | ||
| 40 | -+# information regarding copyright ownership. | ||
| 41 | -+ | ||
| 42 | -+from server import main | ||
| 43 | -+ | ||
| 44 | -+ | ||
| 45 | -+if __name__ == "__main__": | ||
| 46 | -+ main() | ||
| 47 | -diff --git a/bin/tests/system/repro_5874_nsec3_parent/ns2/named.conf.j2 b/bin/tests/system/repro_5874_nsec3_parent/ns2/named.conf.j2 | ||
| 48 | -new file mode 100644 | ||
| 49 | -index 0000000000..9358866373 | ||
| 50 | ---- /dev/null | ||
| 51 | -+++ b/bin/tests/system/repro_5874_nsec3_parent/ns2/named.conf.j2 | ||
| 52 | -@@ -0,0 +1,35 @@ | ||
| 53 | -+// validating resolver | ||
| 54 | -+ | ||
| 55 | -+options { | ||
| 56 | -+ query-source address 10.53.0.2; | ||
| 57 | -+ notify-source 10.53.0.2; | ||
| 58 | -+ transfer-source 10.53.0.2; | ||
| 59 | -+ port @PORT@; | ||
| 60 | -+ pid-file "named.pid"; | ||
| 61 | -+ listen-on { 10.53.0.2; }; | ||
| 62 | -+ listen-on-v6 { none; }; | ||
| 63 | -+ recursion yes; | ||
| 64 | -+ dnssec-validation yes; | ||
| 65 | -+ trust-anchor-telemetry no; | ||
| 66 | -+ resolver-query-timeout 5000; | ||
| 67 | -+}; | ||
| 68 | -+ | ||
| 69 | -+controls { | ||
| 70 | -+ inet 10.53.0.2 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; | ||
| 71 | -+}; | ||
| 72 | -+ | ||
| 73 | -+include "../../_common/rndc.key"; | ||
| 74 | -+ | ||
| 75 | -+zone "." { | ||
| 76 | -+ type hint; | ||
| 77 | -+ file "../../_common/root.hint"; | ||
| 78 | -+}; | ||
| 79 | -+ | ||
| 80 | -+zone "tld.test" { | ||
| 81 | -+ type static-stub; | ||
| 82 | -+ server-addresses { 10.53.0.1; }; | ||
| 83 | -+}; | ||
| 84 | -+ | ||
| 85 | -+trust-anchors { | ||
| 86 | -+ tld.test. static-key 257 3 13 "@TLD_DNSKEY@"; | ||
| 87 | -+}; | ||
| 88 | -diff --git a/bin/tests/system/repro_5874_nsec3_parent/server.py b/bin/tests/system/repro_5874_nsec3_parent/server.py | ||
| 89 | -new file mode 100644 | ||
| 90 | -index 0000000000..c6856bb228 | ||
| 91 | ---- /dev/null | ||
| 92 | -+++ b/bin/tests/system/repro_5874_nsec3_parent/server.py | ||
| 93 | -@@ -0,0 +1,249 @@ | ||
| 94 | -+#!/usr/bin/python3 | ||
| 95 | -+ | ||
| 96 | -+# Copyright (C) Internet Systems Consortium, Inc. ("ISC") | ||
| 97 | -+# | ||
| 98 | -+# SPDX-License-Identifier: MPL-2.0 | ||
| 99 | -+# | ||
| 100 | -+# This Source Code Form is subject to the terms of the Mozilla Public | ||
| 101 | -+# License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| 102 | -+# file, you can obtain one at https://mozilla.org/MPL/2.0/. | ||
| 103 | -+# | ||
| 104 | -+# See the COPYRIGHT file distributed with this work for additional | ||
| 105 | -+# information regarding copyright ownership. | ||
| 106 | -+ | ||
| 107 | -+from collections.abc import AsyncGenerator | ||
| 108 | -+from dataclasses import dataclass | ||
| 109 | -+from pathlib import Path | ||
| 110 | -+ | ||
| 111 | -+import json | ||
| 112 | -+ | ||
| 113 | -+from cryptography.hazmat.primitives import serialization | ||
| 114 | -+ | ||
| 115 | -+import dns.dnssec | ||
| 116 | -+import dns.flags | ||
| 117 | -+import dns.message | ||
| 118 | -+import dns.name | ||
| 119 | -+import dns.rdata | ||
| 120 | -+import dns.rdataclass | ||
| 121 | -+import dns.rcode | ||
| 122 | -+import dns.rdatatype | ||
| 123 | -+import dns.rrset | ||
| 124 | -+ | ||
| 125 | -+from isctest.asyncserver import ( | ||
| 126 | -+ AsyncDnsServer, | ||
| 127 | -+ DnsResponseSend, | ||
| 128 | -+ QueryContext, | ||
| 129 | -+ ResponseHandler, | ||
| 130 | -+) | ||
| 131 | -+ | ||
| 132 | -+TTL = 300 | ||
| 133 | -+TLD = "tld.test." | ||
| 134 | -+APEX_HASH = "1B40241KFORIOG780N4IKSCRLVETPCTQ" | ||
| 135 | -+ATTACKER = f"{APEX_HASH.lower()}.{TLD}" | ||
| 136 | -+VICTIM = f"victim.{TLD}" | ||
| 137 | -+AUTH_IP = "10.53.0.1" | ||
| 138 | -+ | ||
| 139 | -+ | ||
| 140 | -+@dataclass(frozen=True) | ||
| 141 | -+class Key: | ||
| 142 | -+ zone: dns.name.Name | ||
| 143 | -+ private_key: object | ||
| 144 | -+ dnskey: dns.rdata.Rdata | ||
| 145 | -+ ds: dns.rdata.Rdata | ||
| 146 | -+ | ||
| 147 | -+ | ||
| 148 | -+def name(text: str) -> dns.name.Name: | ||
| 149 | -+ return dns.name.from_text(text) | ||
| 150 | -+ | ||
| 151 | -+ | ||
| 152 | -+def load_keys() -> dict[str, Key]: | ||
| 153 | -+ path = Path(__file__).resolve().parent / "keys.json" | ||
| 154 | -+ with path.open(encoding="utf-8") as keys_file: | ||
| 155 | -+ raw_keys = json.load(keys_file) | ||
| 156 | -+ | ||
| 157 | -+ keys = {} | ||
| 158 | -+ for zone, raw_key in raw_keys.items(): | ||
| 159 | -+ private_key = serialization.load_pem_private_key( | ||
| 160 | -+ raw_key["private_pem"].encode("ascii"), | ||
| 161 | -+ password=None, | ||
| 162 | -+ ) | ||
| 163 | -+ dnskey = dns.rdata.from_text( | ||
| 164 | -+ dns.rdataclass.IN, dns.rdatatype.DNSKEY, raw_key["dnskey"] | ||
| 165 | -+ ) | ||
| 166 | -+ ds = dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.DS, raw_key["ds"]) | ||
| 167 | -+ keys[zone] = Key(name(zone), private_key, dnskey, ds) | ||
| 168 | -+ | ||
| 169 | -+ return keys | ||
| 170 | -+ | ||
| 171 | -+ | ||
| 172 | -+def rrset(owner: str, rdtype: dns.rdatatype.RdataType, *rdatas: str) -> dns.rrset.RRset: | ||
| 173 | -+ return dns.rrset.from_text(owner, TTL, dns.rdataclass.IN, rdtype, *rdatas) | ||
| 174 | -+ | ||
| 175 | -+ | ||
| 176 | -+def rrset_from_rdata(owner: str, rdata: dns.rdata.Rdata) -> dns.rrset.RRset: | ||
| 177 | -+ return dns.rrset.from_rdata(name(owner), TTL, rdata) | ||
| 178 | -+ | ||
| 179 | -+ | ||
| 180 | -+def rrsig_rrset(covered: dns.rrset.RRset, signer: Key) -> dns.rrset.RRset: | ||
| 181 | -+ rrsig = dns.dnssec.sign( | ||
| 182 | -+ covered, | ||
| 183 | -+ signer.private_key, | ||
| 184 | -+ signer.zone, | ||
| 185 | -+ signer.dnskey, | ||
| 186 | -+ lifetime=86400, | ||
| 187 | -+ verify=True, | ||
| 188 | -+ ) | ||
| 189 | -+ return dns.rrset.from_rdata(covered.name, covered.ttl, rrsig) | ||
| 190 | -+ | ||
| 191 | -+ | ||
| 192 | -+def add_signed( | ||
| 193 | -+ section: list[dns.rrset.RRset], covered: dns.rrset.RRset, signer: Key | ||
| 194 | -+) -> None: | ||
| 195 | -+ section.append(covered) | ||
| 196 | -+ section.append(rrsig_rrset(covered, signer)) | ||
| 197 | -+ | ||
| 198 | -+ | ||
| 199 | -+def dnskey_rrset(zone: str, zone_key: Key) -> dns.rrset.RRset: | ||
| 200 | -+ return rrset_from_rdata(zone, zone_key.dnskey) | ||
| 201 | -+ | ||
| 202 | -+ | ||
| 203 | -+def ds_rrset(zone: str, zone_key: Key) -> dns.rrset.RRset: | ||
| 204 | -+ return rrset_from_rdata(zone, zone_key.ds) | ||
| 205 | -+ | ||
| 206 | -+ | ||
| 207 | -+def soa_rrset(zone: str) -> dns.rrset.RRset: | ||
| 208 | -+ return rrset( | ||
| 209 | -+ zone, | ||
| 210 | -+ dns.rdatatype.SOA, | ||
| 211 | -+ f"ns.{zone} hostmaster.{zone} 1 3600 600 86400 300", | ||
| 212 | -+ ) | ||
| 213 | -+ | ||
| 214 | -+ | ||
| 215 | -+def ns_rrset(zone: str, ns_target: str) -> dns.rrset.RRset: | ||
| 216 | -+ return rrset(zone, dns.rdatatype.NS, ns_target) | ||
| 217 | -+ | ||
| 218 | -+ | ||
| 219 | -+def glue_rrset(ns_target: str, address: str) -> dns.rrset.RRset: | ||
| 220 | -+ return rrset(ns_target, dns.rdatatype.A, address) | ||
| 221 | -+ | ||
| 222 | -+ | ||
| 223 | -+def answer_dnskey(response: dns.message.Message, zone: str, zone_key: Key) -> None: | ||
| 224 | -+ add_signed(response.answer, dnskey_rrset(zone, zone_key), zone_key) | ||
| 225 | -+ | ||
| 226 | -+ | ||
| 227 | -+def answer_soa(response: dns.message.Message, zone: str, zone_key: Key) -> None: | ||
| 228 | -+ add_signed(response.answer, soa_rrset(zone), zone_key) | ||
| 229 | -+ | ||
| 230 | -+ | ||
| 231 | -+def answer_ns( | ||
| 232 | -+ response: dns.message.Message, zone: str, ns_target: str, zone_key: Key | ||
| 233 | -+) -> None: | ||
| 234 | -+ add_signed(response.answer, ns_rrset(zone, ns_target), zone_key) | ||
| 235 | -+ | ||
| 236 | -+ | ||
| 237 | -+def answer_ds( | ||
| 238 | -+ response: dns.message.Message, zone: str, child_key: Key, parent_key: Key | ||
| 239 | -+) -> None: | ||
| 240 | -+ add_signed(response.answer, ds_rrset(zone, child_key), parent_key) | ||
| 241 | -+ | ||
| 242 | -+ | ||
| 243 | -+def child_nsec3_rrset() -> dns.rrset.RRset: | ||
| 244 | -+ rdata = dns.rdata.from_text( | ||
| 245 | -+ dns.rdataclass.IN, | ||
| 246 | -+ dns.rdatatype.NSEC3, | ||
| 247 | -+ f"1 0 0 - {APEX_HASH} NS SOA RRSIG DNSKEY NSEC3PARAM", | ||
| 248 | -+ ) | ||
| 249 | -+ return dns.rrset.from_rdata(name(f"{APEX_HASH}.{TLD}"), TTL, rdata) | ||
| 250 | -+ | ||
| 251 | -+ | ||
| 252 | -+def forged_nxdomain(response: dns.message.Message, keys: dict[str, Key]) -> None: | ||
| 253 | -+ response.set_rcode(dns.rcode.NXDOMAIN) | ||
| 254 | -+ | ||
| 255 | -+ add_signed(response.authority, soa_rrset(TLD), keys[TLD]) | ||
| 256 | -+ | ||
| 257 | -+ # The owner name derives zone "tld.test.", but the RRSIG signer is the | ||
| 258 | -+ # secure child zone "1b40241kforiog780n4ikscrlvetpctq.tld.test.". | ||
| 259 | -+ add_signed(response.authority, child_nsec3_rrset(), keys[ATTACKER]) | ||
| 260 | -+ | ||
| 261 | -+ | ||
| 262 | -+class SignedResponseHandler(ResponseHandler): | ||
| 263 | -+ def __init__(self, keys: dict[str, Key]) -> None: | ||
| 264 | -+ self.keys = keys | ||
| 265 | -+ | ||
| 266 | -+ async def get_responses( | ||
| 267 | -+ self, qctx: QueryContext | ||
| 268 | -+ ) -> AsyncGenerator[DnsResponseSend, None]: | ||
| 269 | -+ qctx.prepare_new_response(with_zone_data=False) | ||
| 270 | -+ qctx.response.flags |= dns.flags.AA | ||
| 271 | -+ qctx.response.set_rcode(dns.rcode.NOERROR) | ||
| 272 | -+ self.respond(qctx) | ||
| 273 | -+ yield DnsResponseSend(qctx.response, authoritative=True) | ||
| 274 | -+ | ||
| 275 | -+ def respond(self, qctx: QueryContext) -> None: | ||
| 276 | -+ raise NotImplementedError | ||
| 277 | -+ | ||
| 278 | -+ | ||
| 279 | -+class VictimForgedNxdomainHandler(SignedResponseHandler): | ||
| 280 | -+ def match(self, qctx: QueryContext) -> bool: | ||
| 281 | -+ return qctx.qname == name(VICTIM) and qctx.qtype == dns.rdatatype.A | ||
| 282 | -+ | ||
| 283 | -+ def respond(self, qctx: QueryContext) -> None: | ||
| 284 | -+ forged_nxdomain(qctx.response, self.keys) | ||
| 285 | -+ | ||
| 286 | -+ | ||
| 287 | -+class ChildDsHandler(SignedResponseHandler): | ||
| 288 | -+ def match(self, qctx: QueryContext) -> bool: | ||
| 289 | -+ return qctx.qname == name(ATTACKER) and qctx.qtype == dns.rdatatype.DS | ||
| 290 | -+ | ||
| 291 | -+ def respond(self, qctx: QueryContext) -> None: | ||
| 292 | -+ answer_ds(qctx.response, ATTACKER, self.keys[ATTACKER], self.keys[TLD]) | ||
| 293 | -+ | ||
| 294 | -+ | ||
| 295 | -+class AttackerZoneHandler(SignedResponseHandler): | ||
| 296 | -+ def match(self, qctx: QueryContext) -> bool: | ||
| 297 | -+ return qctx.qname.is_subdomain(name(ATTACKER)) | ||
| 298 | -+ | ||
| 299 | -+ def respond(self, qctx: QueryContext) -> None: | ||
| 300 | -+ if qctx.qname == name(ATTACKER): | ||
| 301 | -+ if qctx.qtype == dns.rdatatype.DNSKEY: | ||
| 302 | -+ answer_dnskey(qctx.response, ATTACKER, self.keys[ATTACKER]) | ||
| 303 | -+ elif qctx.qtype == dns.rdatatype.SOA: | ||
| 304 | -+ answer_soa(qctx.response, ATTACKER, self.keys[ATTACKER]) | ||
| 305 | -+ else: | ||
| 306 | -+ answer_ns(qctx.response, ATTACKER, f"ns.{ATTACKER}", self.keys[ATTACKER]) | ||
| 307 | -+ qctx.response.additional.append(glue_rrset(f"ns.{ATTACKER}", AUTH_IP)) | ||
| 308 | -+ return | ||
| 309 | -+ | ||
| 310 | -+ qctx.response.set_rcode(dns.rcode.NXDOMAIN) | ||
| 311 | -+ add_signed(qctx.response.authority, soa_rrset(ATTACKER), self.keys[ATTACKER]) | ||
| 312 | -+ | ||
| 313 | -+ | ||
| 314 | -+class TldZoneHandler(SignedResponseHandler): | ||
| 315 | -+ def match(self, qctx: QueryContext) -> bool: | ||
| 316 | -+ return qctx.qname.is_subdomain(name(TLD)) | ||
| 317 | -+ | ||
| 318 | -+ def respond(self, qctx: QueryContext) -> None: | ||
| 319 | -+ if qctx.qname == name(TLD): | ||
| 320 | -+ if qctx.qtype == dns.rdatatype.DNSKEY: | ||
| 321 | -+ answer_dnskey(qctx.response, TLD, self.keys[TLD]) | ||
| 322 | -+ elif qctx.qtype == dns.rdatatype.SOA: | ||
| 323 | -+ answer_soa(qctx.response, TLD, self.keys[TLD]) | ||
| 324 | -+ else: | ||
| 325 | -+ answer_ns(qctx.response, TLD, "ns.tld.test.", self.keys[TLD]) | ||
| 326 | -+ qctx.response.additional.append(glue_rrset("ns.tld.test.", AUTH_IP)) | ||
| 327 | -+ return | ||
| 328 | -+ | ||
| 329 | -+ qctx.response.set_rcode(dns.rcode.NXDOMAIN) | ||
| 330 | -+ add_signed(qctx.response.authority, soa_rrset(TLD), self.keys[TLD]) | ||
| 331 | -+ | ||
| 332 | -+ | ||
| 333 | -+def main() -> None: | ||
| 334 | -+ keys = load_keys() | ||
| 335 | -+ server = AsyncDnsServer(default_aa=True) | ||
| 336 | -+ server.install_response_handlers( | ||
| 337 | -+ VictimForgedNxdomainHandler(keys), | ||
| 338 | -+ ChildDsHandler(keys), | ||
| 339 | -+ AttackerZoneHandler(keys), | ||
| 340 | -+ TldZoneHandler(keys), | ||
| 341 | -+ ) | ||
| 342 | -+ server.run() | ||
| 343 | -diff --git a/bin/tests/system/repro_5874_nsec3_parent/tests_repro_5874_nsec3_parent.py b/bin/tests/system/repro_5874_nsec3_parent/tests_repro_5874_nsec3_parent.py | ||
| 344 | -new file mode 100644 | ||
| 345 | -index 0000000000..5fbeff5bef | ||
| 346 | ---- /dev/null | ||
| 347 | -+++ b/bin/tests/system/repro_5874_nsec3_parent/tests_repro_5874_nsec3_parent.py | ||
| 348 | -@@ -0,0 +1,144 @@ | ||
| 349 | -+#!/usr/bin/python3 | ||
| 350 | -+ | ||
| 351 | -+# Copyright (C) Internet Systems Consortium, Inc. ("ISC") | ||
| 352 | -+# | ||
| 353 | -+# SPDX-License-Identifier: MPL-2.0 | ||
| 354 | -+# | ||
| 355 | -+# This Source Code Form is subject to the terms of the Mozilla Public | ||
| 356 | -+# License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| 357 | -+# file, you can obtain one at https://mozilla.org/MPL/2.0/. | ||
| 358 | -+# | ||
| 359 | -+# See the COPYRIGHT file distributed with this work for additional | ||
| 360 | -+# information regarding copyright ownership. | ||
| 361 | -+ | ||
| 362 | -+from pathlib import Path | ||
| 363 | -+ | ||
| 364 | -+import json | ||
| 365 | -+ | ||
| 366 | -+from cryptography.hazmat.primitives import serialization | ||
| 367 | -+from cryptography.hazmat.primitives.asymmetric import ec | ||
| 368 | -+ | ||
| 369 | -+import dns.dnssec | ||
| 370 | -+import dns.flags | ||
| 371 | -+import dns.name | ||
| 372 | -+import dns.rdataclass | ||
| 373 | -+import dns.rdatatype | ||
| 374 | -+import pytest | ||
| 375 | -+ | ||
| 376 | -+import isctest | ||
| 377 | -+ | ||
| 378 | -+APEX_HASH = "1B40241KFORIOG780N4IKSCRLVETPCTQ" | ||
| 379 | -+ATTACKER = f"{APEX_HASH.lower()}.tld.test." | ||
| 380 | -+VICTIM = "victim.tld.test." | ||
| 381 | -+AUTH = "10.53.0.1" | ||
| 382 | -+RESOLVER = "10.53.0.2" | ||
| 383 | -+ | ||
| 384 | -+pytestmark = pytest.mark.extra_artifacts( | ||
| 385 | -+ [ | ||
| 386 | -+ "ans*/ans.run", | ||
| 387 | -+ "keys.json", | ||
| 388 | -+ ] | ||
| 389 | -+) | ||
| 390 | -+ | ||
| 391 | -+ | ||
| 392 | -+def _make_key(zone): | ||
| 393 | -+ private_key = ec.generate_private_key(ec.SECP256R1()) | ||
| 394 | -+ dnskey = dns.dnssec.make_dnskey( | ||
| 395 | -+ private_key.public_key(), | ||
| 396 | -+ algorithm="ECDSAP256SHA256", | ||
| 397 | -+ flags=257, | ||
| 398 | -+ ) | ||
| 399 | -+ ds = dns.dnssec.make_ds(dns.name.from_text(zone), dnskey, "SHA256") | ||
| 400 | -+ private_pem = private_key.private_bytes( | ||
| 401 | -+ encoding=serialization.Encoding.PEM, | ||
| 402 | -+ format=serialization.PrivateFormat.PKCS8, | ||
| 403 | -+ encryption_algorithm=serialization.NoEncryption(), | ||
| 404 | -+ ).decode("ascii") | ||
| 405 | -+ return { | ||
| 406 | -+ "private_pem": private_pem, | ||
| 407 | -+ "dnskey": dnskey.to_text(), | ||
| 408 | -+ "ds": ds.to_text(), | ||
| 409 | -+ } | ||
| 410 | -+ | ||
| 411 | -+ | ||
| 412 | -+def bootstrap(): | ||
| 413 | -+ zones = ["tld.test.", ATTACKER] | ||
| 414 | -+ keys = {zone: _make_key(zone) for zone in zones} | ||
| 415 | -+ | ||
| 416 | -+ Path("keys.json").write_text(json.dumps(keys, indent=2), encoding="ascii") | ||
| 417 | -+ | ||
| 418 | -+ tld_dnskey = "".join(keys["tld.test."]["dnskey"].split()[3:]) | ||
| 419 | -+ return {"TLD_DNSKEY": tld_dnskey} | ||
| 420 | -+ | ||
| 421 | -+ | ||
| 422 | -+def _check_direct_dnskey_response(zone): | ||
| 423 | -+ query = isctest.query.create(zone, "DNSKEY") | ||
| 424 | -+ response = isctest.query.tcp(query, AUTH) | ||
| 425 | -+ | ||
| 426 | -+ isctest.check.noerror(response) | ||
| 427 | -+ assert response.flags & dns.flags.AA | ||
| 428 | -+ assert ( | ||
| 429 | -+ response.get_rrset( | ||
| 430 | -+ response.answer, | ||
| 431 | -+ dns.name.from_text(zone), | ||
| 432 | -+ dns.rdataclass.IN, | ||
| 433 | -+ dns.rdatatype.DNSKEY, | ||
| 434 | -+ ) | ||
| 435 | -+ is not None | ||
| 436 | -+ ), response | ||
| 437 | -+ | ||
| 438 | -+ | ||
| 439 | -+def _check_direct_ds_response(zone): | ||
| 440 | -+ query = isctest.query.create(zone, "DS") | ||
| 441 | -+ response = isctest.query.tcp(query, AUTH) | ||
| 442 | -+ | ||
| 443 | -+ isctest.check.noerror(response) | ||
| 444 | -+ assert response.flags & dns.flags.AA | ||
| 445 | -+ assert ( | ||
| 446 | -+ response.get_rrset( | ||
| 447 | -+ response.answer, | ||
| 448 | -+ dns.name.from_text(zone), | ||
| 449 | -+ dns.rdataclass.IN, | ||
| 450 | -+ dns.rdatatype.DS, | ||
| 451 | -+ ) | ||
| 452 | -+ is not None | ||
| 453 | -+ ), response | ||
| 454 | -+ | ||
| 455 | -+ | ||
| 456 | -+def test_repro_5874_direct_forged_nsec3_response_has_child_signer(): | ||
| 457 | -+ _check_direct_dnskey_response("tld.test.") | ||
| 458 | -+ _check_direct_dnskey_response(ATTACKER) | ||
| 459 | -+ _check_direct_ds_response(ATTACKER) | ||
| 460 | -+ | ||
| 461 | -+ query = isctest.query.create(VICTIM, "A") | ||
| 462 | -+ response = isctest.query.tcp(query, AUTH) | ||
| 463 | -+ | ||
| 464 | -+ isctest.check.nxdomain(response) | ||
| 465 | -+ assert response.flags & dns.flags.AA | ||
| 466 | -+ | ||
| 467 | -+ nsec3_owner = dns.name.from_text(f"{APEX_HASH}.tld.test.") | ||
| 468 | -+ nsec3 = response.get_rrset( | ||
| 469 | -+ response.authority, | ||
| 470 | -+ nsec3_owner, | ||
| 471 | -+ dns.rdataclass.IN, | ||
| 472 | -+ dns.rdatatype.NSEC3, | ||
| 473 | -+ ) | ||
| 474 | -+ rrsig = response.get_rrset( | ||
| 475 | -+ response.authority, | ||
| 476 | -+ nsec3_owner, | ||
| 477 | -+ dns.rdataclass.IN, | ||
| 478 | -+ dns.rdatatype.RRSIG, | ||
| 479 | -+ covers=dns.rdatatype.NSEC3, | ||
| 480 | -+ ) | ||
| 481 | -+ | ||
| 482 | -+ assert nsec3 is not None, response | ||
| 483 | -+ assert rrsig is not None, response | ||
| 484 | -+ assert rrsig[0].signer == dns.name.from_text(ATTACKER) | ||
| 485 | -+ | ||
| 486 | -+ | ||
| 487 | -+def test_repro_5874_resolver_rejects_child_signed_nsec3_parent_proof(): | ||
| 488 | -+ query = isctest.query.create(VICTIM, "A") | ||
| 489 | -+ response = isctest.query.tcp(query, RESOLVER) | ||
| 490 | -+ | ||
| 491 | -+ isctest.check.servfail(response) | ||
| 492 | -+ isctest.check.noadflag(response) | ||
| 493 | --- | ||
| 494 | -2.43.0 | ||
| 495 | - | ||
| @@ -1,309 +0,0 @@ | |||
| 1 | -From f4167e8b76e614e52245b82caae1a97da6800183 Mon Sep 17 00:00:00 2001 | ||
| 2 | -From: Matthijs Mekking <matthijs@isc.org> | ||
| 3 | -Date: Thu, 4 Jun 2026 16:12:50 +0200 | ||
| 4 | -Subject: [PATCH] Update reproducer #5874 | ||
| 5 | - | ||
| 6 | -Update the llm generated reproducer: | ||
| 7 | -- Move server.py into ans1/ans.py | ||
| 8 | -- Remove unnecessary named.conf configuration options | ||
| 9 | -- Add comments describing the steps (copied from GL issue) | ||
| 10 | -- Rename system test | ||
| 11 | - | ||
| 12 | -(cherry picked from commit c1321fef165a2ef8c2bff971901c58941e8e694c) | ||
| 13 | - | ||
| 14 | -Conflict:no | ||
| 15 | -Reference:https://gitlab.isc.org/isc-projects/bind9/-/commit/f4167e8b76e614e52245b82caae1a97da6800183 | ||
| 16 | ---- | ||
| 17 | - .../ans1/ans.py} | 81 +++++++++++++------ | ||
| 18 | - .../ns2/named.conf.j2 | 2 - | ||
| 19 | - .../tests_nsec3_impersonation.py} | 38 +++++---- | ||
| 20 | - .../repro_5874_nsec3_parent/ans1/ans.py | 18 ----- | ||
| 21 | - 4 files changed, 79 insertions(+), 60 deletions(-) | ||
| 22 | - rename bin/tests/system/{repro_5874_nsec3_parent/server.py => nsec3_impersonation/ans1/ans.py} (83%) | ||
| 23 | - rename bin/tests/system/{repro_5874_nsec3_parent => nsec3_impersonation}/ns2/named.conf.j2 (90%) | ||
| 24 | - rename bin/tests/system/{repro_5874_nsec3_parent/tests_repro_5874_nsec3_parent.py => nsec3_impersonation/tests_nsec3_impersonation.py} (84%) | ||
| 25 | - delete mode 100644 bin/tests/system/repro_5874_nsec3_parent/ans1/ans.py | ||
| 26 | - | ||
| 27 | -diff --git a/bin/tests/system/repro_5874_nsec3_parent/server.py b/bin/tests/system/nsec3_impersonation/ans1/ans.py | ||
| 28 | -similarity index 83% | ||
| 29 | -rename from bin/tests/system/repro_5874_nsec3_parent/server.py | ||
| 30 | -rename to bin/tests/system/nsec3_impersonation/ans1/ans.py | ||
| 31 | -index c6856bb228..177e79c195 100644 | ||
| 32 | ---- a/bin/tests/system/repro_5874_nsec3_parent/server.py | ||
| 33 | -+++ b/bin/tests/system/nsec3_impersonation/ans1/ans.py | ||
| 34 | -@@ -23,9 +23,9 @@ import dns.dnssec | ||
| 35 | - import dns.flags | ||
| 36 | - import dns.message | ||
| 37 | - import dns.name | ||
| 38 | -+import dns.rcode | ||
| 39 | - import dns.rdata | ||
| 40 | - import dns.rdataclass | ||
| 41 | --import dns.rcode | ||
| 42 | - import dns.rdatatype | ||
| 43 | - import dns.rrset | ||
| 44 | - | ||
| 45 | -@@ -141,10 +141,21 @@ def answer_ns( | ||
| 46 | - add_signed(response.answer, ns_rrset(zone, ns_target), zone_key) | ||
| 47 | - | ||
| 48 | - | ||
| 49 | --def answer_ds( | ||
| 50 | -- response: dns.message.Message, zone: str, child_key: Key, parent_key: Key | ||
| 51 | --) -> None: | ||
| 52 | -- add_signed(response.answer, ds_rrset(zone, child_key), parent_key) | ||
| 53 | -+class SignedResponseHandler(ResponseHandler): | ||
| 54 | -+ def __init__(self, keys: dict[str, Key]) -> None: | ||
| 55 | -+ self.keys = keys | ||
| 56 | -+ | ||
| 57 | -+ async def get_responses( | ||
| 58 | -+ self, qctx: QueryContext | ||
| 59 | -+ ) -> AsyncGenerator[DnsResponseSend, None]: | ||
| 60 | -+ qctx.prepare_new_response(with_zone_data=False) | ||
| 61 | -+ qctx.response.flags |= dns.flags.AA | ||
| 62 | -+ qctx.response.set_rcode(dns.rcode.NOERROR) | ||
| 63 | -+ self.respond(qctx) | ||
| 64 | -+ yield DnsResponseSend(qctx.response, authoritative=True) | ||
| 65 | -+ | ||
| 66 | -+ def respond(self, qctx: QueryContext) -> None: | ||
| 67 | -+ raise NotImplementedError | ||
| 68 | - | ||
| 69 | - | ||
| 70 | - def child_nsec3_rrset() -> dns.rrset.RRset: | ||
| 71 | -@@ -162,28 +173,15 @@ def forged_nxdomain(response: dns.message.Message, keys: dict[str, Key]) -> None | ||
| 72 | - add_signed(response.authority, soa_rrset(TLD), keys[TLD]) | ||
| 73 | - | ||
| 74 | - # The owner name derives zone "tld.test.", but the RRSIG signer is the | ||
| 75 | -- # secure child zone "1b40241kforiog780n4ikscrlvetpctq.tld.test.". | ||
| 76 | -+ # malicious child zone "1b40241kforiog780n4ikscrlvetpctq.tld.test.". | ||
| 77 | - add_signed(response.authority, child_nsec3_rrset(), keys[ATTACKER]) | ||
| 78 | - | ||
| 79 | - | ||
| 80 | --class SignedResponseHandler(ResponseHandler): | ||
| 81 | -- def __init__(self, keys: dict[str, Key]) -> None: | ||
| 82 | -- self.keys = keys | ||
| 83 | -- | ||
| 84 | -- async def get_responses( | ||
| 85 | -- self, qctx: QueryContext | ||
| 86 | -- ) -> AsyncGenerator[DnsResponseSend, None]: | ||
| 87 | -- qctx.prepare_new_response(with_zone_data=False) | ||
| 88 | -- qctx.response.flags |= dns.flags.AA | ||
| 89 | -- qctx.response.set_rcode(dns.rcode.NOERROR) | ||
| 90 | -- self.respond(qctx) | ||
| 91 | -- yield DnsResponseSend(qctx.response, authoritative=True) | ||
| 92 | -- | ||
| 93 | -- def respond(self, qctx: QueryContext) -> None: | ||
| 94 | -- raise NotImplementedError | ||
| 95 | -- | ||
| 96 | -- | ||
| 97 | - class VictimForgedNxdomainHandler(SignedResponseHandler): | ||
| 98 | -+ """ | ||
| 99 | -+ This serves the forged response for the victim's domain. | ||
| 100 | -+ """ | ||
| 101 | -+ | ||
| 102 | - def match(self, qctx: QueryContext) -> bool: | ||
| 103 | - return qctx.qname == name(VICTIM) and qctx.qtype == dns.rdatatype.A | ||
| 104 | - | ||
| 105 | -@@ -192,14 +190,31 @@ class VictimForgedNxdomainHandler(SignedResponseHandler): | ||
| 106 | - | ||
| 107 | - | ||
| 108 | - class ChildDsHandler(SignedResponseHandler): | ||
| 109 | -+ """ | ||
| 110 | -+ This will spoof the response for the malicious zone when qtype is DS. | ||
| 111 | -+ It is actually a validly signed DS response. | ||
| 112 | -+ """ | ||
| 113 | -+ | ||
| 114 | - def match(self, qctx: QueryContext) -> bool: | ||
| 115 | - return qctx.qname == name(ATTACKER) and qctx.qtype == dns.rdatatype.DS | ||
| 116 | - | ||
| 117 | - def respond(self, qctx: QueryContext) -> None: | ||
| 118 | -- answer_ds(qctx.response, ATTACKER, self.keys[ATTACKER], self.keys[TLD]) | ||
| 119 | -+ response = qctx.response | ||
| 120 | -+ zone = ATTACKER | ||
| 121 | -+ child_key = self.keys[ATTACKER] | ||
| 122 | -+ parent_key = self.keys[TLD] | ||
| 123 | -+ | ||
| 124 | -+ add_signed(response.answer, ds_rrset(zone, child_key), parent_key) | ||
| 125 | - | ||
| 126 | - | ||
| 127 | - class AttackerZoneHandler(SignedResponseHandler): | ||
| 128 | -+ """ | ||
| 129 | -+ Acts as the malicious authoritative name server. The zone being served | ||
| 130 | -+ is the hashed label of the parent zone (tld.test). This will respond | ||
| 131 | -+ for all queries qtype SOA, DNSKEY, NS at the apex. Any names below | ||
| 132 | -+ the apex are answered with an NXDOMAIN with no NSEC or NSEC3 present. | ||
| 133 | -+ """ | ||
| 134 | -+ | ||
| 135 | - def match(self, qctx: QueryContext) -> bool: | ||
| 136 | - return qctx.qname.is_subdomain(name(ATTACKER)) | ||
| 137 | - | ||
| 138 | -@@ -210,7 +225,9 @@ class AttackerZoneHandler(SignedResponseHandler): | ||
| 139 | - elif qctx.qtype == dns.rdatatype.SOA: | ||
| 140 | - answer_soa(qctx.response, ATTACKER, self.keys[ATTACKER]) | ||
| 141 | - else: | ||
| 142 | -- answer_ns(qctx.response, ATTACKER, f"ns.{ATTACKER}", self.keys[ATTACKER]) | ||
| 143 | -+ answer_ns( | ||
| 144 | -+ qctx.response, ATTACKER, f"ns.{ATTACKER}", self.keys[ATTACKER] | ||
| 145 | -+ ) | ||
| 146 | - qctx.response.additional.append(glue_rrset(f"ns.{ATTACKER}", AUTH_IP)) | ||
| 147 | - return | ||
| 148 | - | ||
| 149 | -@@ -219,6 +236,16 @@ class AttackerZoneHandler(SignedResponseHandler): | ||
| 150 | - | ||
| 151 | - | ||
| 152 | - class TldZoneHandler(SignedResponseHandler): | ||
| 153 | -+ """ | ||
| 154 | -+ Acts as the TLD who is being used in the attack, but is not a standard | ||
| 155 | -+ name server. It only responds with validly signed records for DNSKEY, SOA | ||
| 156 | -+ and NS on the apex. Any names below the apex are answered with an NXDOMAIN | ||
| 157 | -+ with no NSEC or NSEC3 present. | ||
| 158 | -+ | ||
| 159 | -+ If we turn this into a regular name server than the attack won't work. | ||
| 160 | -+ The attack assumes that the adversary can inject these responses on-path. | ||
| 161 | -+ """ | ||
| 162 | -+ | ||
| 163 | - def match(self, qctx: QueryContext) -> bool: | ||
| 164 | - return qctx.qname.is_subdomain(name(TLD)) | ||
| 165 | - | ||
| 166 | -@@ -247,3 +274,7 @@ def main() -> None: | ||
| 167 | - TldZoneHandler(keys), | ||
| 168 | - ) | ||
| 169 | - server.run() | ||
| 170 | -+ | ||
| 171 | -+ | ||
| 172 | -+if __name__ == "__main__": | ||
| 173 | -+ main() | ||
| 174 | -diff --git a/bin/tests/system/repro_5874_nsec3_parent/ns2/named.conf.j2 b/bin/tests/system/nsec3_impersonation/ns2/named.conf.j2 | ||
| 175 | -similarity index 90% | ||
| 176 | -rename from bin/tests/system/repro_5874_nsec3_parent/ns2/named.conf.j2 | ||
| 177 | -rename to bin/tests/system/nsec3_impersonation/ns2/named.conf.j2 | ||
| 178 | -index 9358866373..2c9b0bba9e 100644 | ||
| 179 | ---- a/bin/tests/system/repro_5874_nsec3_parent/ns2/named.conf.j2 | ||
| 180 | -+++ b/bin/tests/system/nsec3_impersonation/ns2/named.conf.j2 | ||
| 181 | -@@ -10,8 +10,6 @@ options { | ||
| 182 | - listen-on-v6 { none; }; | ||
| 183 | - recursion yes; | ||
| 184 | - dnssec-validation yes; | ||
| 185 | -- trust-anchor-telemetry no; | ||
| 186 | -- resolver-query-timeout 5000; | ||
| 187 | - }; | ||
| 188 | - | ||
| 189 | - controls { | ||
| 190 | -diff --git a/bin/tests/system/repro_5874_nsec3_parent/tests_repro_5874_nsec3_parent.py b/bin/tests/system/nsec3_impersonation/tests_nsec3_impersonation.py | ||
| 191 | -similarity index 84% | ||
| 192 | -rename from bin/tests/system/repro_5874_nsec3_parent/tests_repro_5874_nsec3_parent.py | ||
| 193 | -rename to bin/tests/system/nsec3_impersonation/tests_nsec3_impersonation.py | ||
| 194 | -index 5fbeff5bef..bd9bd275b6 100644 | ||
| 195 | ---- a/bin/tests/system/repro_5874_nsec3_parent/tests_repro_5874_nsec3_parent.py | ||
| 196 | -+++ b/bin/tests/system/nsec3_impersonation/tests_nsec3_impersonation.py | ||
| 197 | -@@ -26,6 +26,7 @@ import dns.rdatatype | ||
| 198 | - import pytest | ||
| 199 | - | ||
| 200 | - import isctest | ||
| 201 | -+import isctest.mark | ||
| 202 | - | ||
| 203 | - APEX_HASH = "1B40241KFORIOG780N4IKSCRLVETPCTQ" | ||
| 204 | - ATTACKER = f"{APEX_HASH.lower()}.tld.test." | ||
| 205 | -@@ -33,12 +34,15 @@ VICTIM = "victim.tld.test." | ||
| 206 | - AUTH = "10.53.0.1" | ||
| 207 | - RESOLVER = "10.53.0.2" | ||
| 208 | - | ||
| 209 | --pytestmark = pytest.mark.extra_artifacts( | ||
| 210 | -- [ | ||
| 211 | -- "ans*/ans.run", | ||
| 212 | -- "keys.json", | ||
| 213 | -- ] | ||
| 214 | --) | ||
| 215 | -+pytestmark = [ | ||
| 216 | -+ isctest.mark.with_ecdsa_deterministic, | ||
| 217 | -+ pytest.mark.extra_artifacts( | ||
| 218 | -+ [ | ||
| 219 | -+ "ans*/ans.run", | ||
| 220 | -+ "ans*/keys.json", | ||
| 221 | -+ ] | ||
| 222 | -+ ), | ||
| 223 | -+] | ||
| 224 | - | ||
| 225 | - | ||
| 226 | - def _make_key(zone): | ||
| 227 | -@@ -65,13 +69,13 @@ def bootstrap(): | ||
| 228 | - zones = ["tld.test.", ATTACKER] | ||
| 229 | - keys = {zone: _make_key(zone) for zone in zones} | ||
| 230 | - | ||
| 231 | -- Path("keys.json").write_text(json.dumps(keys, indent=2), encoding="ascii") | ||
| 232 | -+ Path("ans1/keys.json").write_text(json.dumps(keys, indent=2), encoding="ascii") | ||
| 233 | - | ||
| 234 | - tld_dnskey = "".join(keys["tld.test."]["dnskey"].split()[3:]) | ||
| 235 | - return {"TLD_DNSKEY": tld_dnskey} | ||
| 236 | - | ||
| 237 | - | ||
| 238 | --def _check_direct_dnskey_response(zone): | ||
| 239 | -+def check_dnskey_response(zone): | ||
| 240 | - query = isctest.query.create(zone, "DNSKEY") | ||
| 241 | - response = isctest.query.tcp(query, AUTH) | ||
| 242 | - | ||
| 243 | -@@ -88,7 +92,7 @@ def _check_direct_dnskey_response(zone): | ||
| 244 | - ), response | ||
| 245 | - | ||
| 246 | - | ||
| 247 | --def _check_direct_ds_response(zone): | ||
| 248 | -+def check_ds_response(zone): | ||
| 249 | - query = isctest.query.create(zone, "DS") | ||
| 250 | - response = isctest.query.tcp(query, AUTH) | ||
| 251 | - | ||
| 252 | -@@ -105,10 +109,10 @@ def _check_direct_ds_response(zone): | ||
| 253 | - ), response | ||
| 254 | - | ||
| 255 | - | ||
| 256 | --def test_repro_5874_direct_forged_nsec3_response_has_child_signer(): | ||
| 257 | -- _check_direct_dnskey_response("tld.test.") | ||
| 258 | -- _check_direct_dnskey_response(ATTACKER) | ||
| 259 | -- _check_direct_ds_response(ATTACKER) | ||
| 260 | -+def test_attack_responses(): | ||
| 261 | -+ check_dnskey_response("tld.test.") | ||
| 262 | -+ check_dnskey_response(ATTACKER) | ||
| 263 | -+ check_ds_response(ATTACKER) | ||
| 264 | - | ||
| 265 | - query = isctest.query.create(VICTIM, "A") | ||
| 266 | - response = isctest.query.tcp(query, AUTH) | ||
| 267 | -@@ -136,9 +140,13 @@ def test_repro_5874_direct_forged_nsec3_response_has_child_signer(): | ||
| 268 | - assert rrsig[0].signer == dns.name.from_text(ATTACKER) | ||
| 269 | - | ||
| 270 | - | ||
| 271 | --def test_repro_5874_resolver_rejects_child_signed_nsec3_parent_proof(): | ||
| 272 | -+def test_nsec3_impersonation(): | ||
| 273 | -+ """ | ||
| 274 | -+ Reproducer for #5874: | ||
| 275 | -+ F-006 DNSSEC Validation Bypass NSEC3 Apex Hash Label Parent Impersonation | ||
| 276 | -+ """ | ||
| 277 | - query = isctest.query.create(VICTIM, "A") | ||
| 278 | - response = isctest.query.tcp(query, RESOLVER) | ||
| 279 | - | ||
| 280 | -- isctest.check.servfail(response) | ||
| 281 | - isctest.check.noadflag(response) | ||
| 282 | -+ isctest.check.servfail(response) | ||
| 283 | -diff --git a/bin/tests/system/repro_5874_nsec3_parent/ans1/ans.py b/bin/tests/system/repro_5874_nsec3_parent/ans1/ans.py | ||
| 284 | -deleted file mode 100644 | ||
| 285 | -index 614f26a014..0000000000 | ||
| 286 | ---- a/bin/tests/system/repro_5874_nsec3_parent/ans1/ans.py | ||
| 287 | -+++ /dev/null | ||
| 288 | -@@ -1,18 +0,0 @@ | ||
| 289 | --#!/usr/bin/python3 | ||
| 290 | -- | ||
| 291 | --# Copyright (C) Internet Systems Consortium, Inc. ("ISC") | ||
| 292 | --# | ||
| 293 | --# SPDX-License-Identifier: MPL-2.0 | ||
| 294 | --# | ||
| 295 | --# This Source Code Form is subject to the terms of the Mozilla Public | ||
| 296 | --# License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| 297 | --# file, you can obtain one at https://mozilla.org/MPL/2.0/. | ||
| 298 | --# | ||
| 299 | --# See the COPYRIGHT file distributed with this work for additional | ||
| 300 | --# information regarding copyright ownership. | ||
| 301 | -- | ||
| 302 | --from server import main | ||
| 303 | -- | ||
| 304 | -- | ||
| 305 | --if __name__ == "__main__": | ||
| 306 | -- main() | ||
| 307 | --- | ||
| 308 | -2.43.0 | ||
| 309 | - | ||
| @@ -1,65 +0,0 @@ | |||
| 1 | -From fc3ca9e494c277350ca0242721425cace79df82d Mon Sep 17 00:00:00 2001 | ||
| 2 | -From: Mark Andrews <marka@isc.org> | ||
| 3 | -Date: Tue, 19 May 2026 10:44:04 +1000 | ||
| 4 | -Subject: [PATCH] Check that dns_name_fromwire honours the active region | ||
| 5 | - | ||
| 6 | -When reading DNS records from the wire the active region of the | ||
| 7 | -source buffer is set to the end of the current record. dns_name_fromwire | ||
| 8 | -should fail if it attempts to read past this setting. | ||
| 9 | - | ||
| 10 | -(cherry picked from commit 3ed821d68b15fe4e6288e3054397d6bce7e65968) | ||
| 11 | - | ||
| 12 | -Conflict:no | ||
| 13 | -Reference:https://gitlab.isc.org/isc-projects/bind9/-/commit/fc3ca9e494c277350ca0242721425cace79df82d | ||
| 14 | ---- | ||
| 15 | - tests/dns/name_test.c | 27 +++++++++++++++++++++++++++ | ||
| 16 | - 1 file changed, 27 insertions(+) | ||
| 17 | - | ||
| 18 | -diff --git a/tests/dns/name_test.c b/tests/dns/name_test.c | ||
| 19 | -index 172b977708..85ffb2d6e5 100644 | ||
| 20 | ---- a/tests/dns/name_test.c | ||
| 21 | -+++ b/tests/dns/name_test.c | ||
| 22 | -@@ -518,6 +518,32 @@ ISC_RUN_TEST_IMPL(fromregion) { | ||
| 23 | - assert_false(dns_name_isabsolute(&name)); | ||
| 24 | - } | ||
| 25 | - | ||
| 26 | -+ISC_RUN_TEST_IMPL(fromwire) { | ||
| 27 | -+ dns_fixedname_t fixed; | ||
| 28 | -+ dns_name_t *name = dns_fixedname_initname(&fixed); | ||
| 29 | -+ isc_buffer_t b; | ||
| 30 | -+ unsigned char source[] = { 0x03, 'o', 'n', 'e', 0x00, 0x03, | ||
| 31 | -+ 't', 'w', 'o', 0x00, 0x05, 't', | ||
| 32 | -+ 'h', 'r', 'e', 'e', 0x00 }; | ||
| 33 | -+ isc_result_t result; | ||
| 34 | -+ | ||
| 35 | -+ isc_buffer_init(&b, source, sizeof(source)); | ||
| 36 | -+ isc_buffer_add(&b, sizeof(source)); | ||
| 37 | -+ isc_buffer_setactive(&b, 10); /* names 'one.' and 'two.' */ | ||
| 38 | -+ | ||
| 39 | -+ /* | ||
| 40 | -+ * We should only be able to read two names from the buffer | ||
| 41 | -+ * as the active region has been set to cover only the first | ||
| 42 | -+ * two. | ||
| 43 | -+ */ | ||
| 44 | -+ result = dns_name_fromwire(name, &b, DNS_DECOMPRESS_NEVER, NULL); | ||
| 45 | -+ assert_int_equal(result, ISC_R_SUCCESS); | ||
| 46 | -+ result = dns_name_fromwire(name, &b, DNS_DECOMPRESS_NEVER, NULL); | ||
| 47 | -+ assert_int_equal(result, ISC_R_SUCCESS); | ||
| 48 | -+ result = dns_name_fromwire(name, &b, DNS_DECOMPRESS_NEVER, NULL); | ||
| 49 | -+ assert_int_not_equal(result, ISC_R_SUCCESS); | ||
| 50 | -+} | ||
| 51 | -+ | ||
| 52 | - /* is trust-anchor-telemetry test */ | ||
| 53 | - ISC_RUN_TEST_IMPL(istat) { | ||
| 54 | - dns_fixedname_t fixed; | ||
| 55 | -@@ -1009,6 +1035,7 @@ ISC_TEST_ENTRY(fullcompare) | ||
| 56 | - ISC_TEST_ENTRY(compression) | ||
| 57 | - ISC_TEST_ENTRY(collision) | ||
| 58 | - ISC_TEST_ENTRY(fromregion) | ||
| 59 | -+ISC_TEST_ENTRY(fromwire) | ||
| 60 | - ISC_TEST_ENTRY(istat) | ||
| 61 | - ISC_TEST_ENTRY(init) | ||
| 62 | - ISC_TEST_ENTRY(invalidate) | ||
| 63 | --- | ||
| 64 | -2.43.0 | ||
| 65 | - | ||
| @@ -1,32 +0,0 @@ | |||
| 1 | -From 1911561e056dca9eba2e54de5264d61b9184f425 Mon Sep 17 00:00:00 2001 | ||
| 2 | -From: Mark Andrews <marka@isc.org> | ||
| 3 | -Date: Tue, 19 May 2026 15:00:17 +1000 | ||
| 4 | -Subject: [PATCH] Fix the yaml query zone name code in dnstap-read | ||
| 5 | - | ||
| 6 | -When the buffer to read the query zone name was constructed | ||
| 7 | -isc_buffer_setactive was not called. This is now needed as | ||
| 8 | -dns_name_fromwire is being corrected to check the active region. | ||
| 9 | - | ||
| 10 | -(cherry picked from commit a25522c28c46655a81d2bf1d96374c81d834b157) | ||
| 11 | - | ||
| 12 | -Conflict:no | ||
| 13 | -Reference:https://gitlab.isc.org/isc-projects/bind9/-/commit/1911561e056dca9eba2e54de5264d61b9184f425 | ||
| 14 | ---- | ||
| 15 | - bin/tools/dnstap-read.c | 1 + | ||
| 16 | - 1 file changed, 1 insertion(+) | ||
| 17 | - | ||
| 18 | -diff --git a/bin/tools/dnstap-read.c b/bin/tools/dnstap-read.c | ||
| 19 | -index cbc82c33c5..a7cd1f19ab 100644 | ||
| 20 | ---- a/bin/tools/dnstap-read.c | ||
| 21 | -+++ b/bin/tools/dnstap-read.c | ||
| 22 | -@@ -308,6 +308,7 @@ print_yaml(dns_dtdata_t *dt) { | ||
| 23 | - | ||
| 24 | - isc_buffer_init(&b, m->query_zone.data, m->query_zone.len); | ||
| 25 | - isc_buffer_add(&b, m->query_zone.len); | ||
| 26 | -+ isc_buffer_setactive(&b, m->query_zone.len); | ||
| 27 | - | ||
| 28 | - result = dns_name_fromwire(name, &b, DNS_DECOMPRESS_NEVER, | ||
| 29 | - NULL); | ||
| 30 | --- | ||
| 31 | -2.43.0 | ||
| 32 | - | ||
| @@ -1,34 +0,0 @@ | |||
| 1 | -From d6aa1ad5b269ab42dd18e7900ac9a0b7013aca20 Mon Sep 17 00:00:00 2001 | ||
| 2 | -From: Mark Andrews <marka@isc.org> | ||
| 3 | -Date: Tue, 19 May 2026 11:12:46 +1000 | ||
| 4 | -Subject: [PATCH] Fix dns_name_fromwire to honour the active region | ||
| 5 | - | ||
| 6 | -dns_name_fromwire was not honouring the source buffer's active | ||
| 7 | -region when reading names from the wire. This allowed malformed | ||
| 8 | -records to be accepted when they shouldn't have been. This has | ||
| 9 | -been corrected. | ||
| 10 | - | ||
| 11 | -(cherry picked from commit 7c4f07a7ef6b571073327b02209df7f75b9363ff) | ||
| 12 | - | ||
| 13 | -Conflict:no | ||
| 14 | -Reference:https://gitlab.isc.org/isc-projects/bind9/-/commit/d6aa1ad5b269ab42dd18e7900ac9a0b7013aca20 | ||
| 15 | ---- | ||
| 16 | - lib/dns/name.c | 2 +- | ||
| 17 | - 1 file changed, 1 insertion(+), 1 deletion(-) | ||
| 18 | - | ||
| 19 | -diff --git a/lib/dns/name.c b/lib/dns/name.c | ||
| 20 | -index 1a900fb5bc..1e8ea1a236 100644 | ||
| 21 | ---- a/lib/dns/name.c | ||
| 22 | -+++ b/lib/dns/name.c | ||
| 23 | -@@ -1492,7 +1492,7 @@ dns_name_fromwire(dns_name_t *const name, isc_buffer_t *const source, | ||
| 24 | - * The amount of the source we consumed is set once. | ||
| 25 | - */ | ||
| 26 | - const uint8_t *const source_buf = isc_buffer_base(source); | ||
| 27 | -- const uint8_t *const source_max = isc_buffer_used(source); | ||
| 28 | -+ const uint8_t *const source_max = isc_buffer_active(source); | ||
| 29 | - const uint8_t *const start = isc_buffer_current(source); | ||
| 30 | - const uint8_t *marker = start; | ||
| 31 | - const uint8_t *cursor = start; | ||
| 32 | --- | ||
| 33 | -2.43.0 | ||
| 34 | - | ||
| @@ -1,38 +0,0 @@ | |||
| 1 | -From 9ddc60767d4614a405b8a2c9938fe00cf60b361a Mon Sep 17 00:00:00 2001 | ||
| 2 | -From: Mark Andrews <marka@isc.org> | ||
| 3 | -Date: Tue, 19 May 2026 11:24:01 +1000 | ||
| 4 | -Subject: [PATCH] Make it clearer that decompression is not allowed here | ||
| 5 | - | ||
| 6 | -For DNSKEY and RRSIG as per RFC 4034, A.1.1. Private Algorithm Types | ||
| 7 | - | ||
| 8 | -For KEY, SIG as per RFC 2535 3.2 The KEY Algorithm Number Specification, | ||
| 9 | -which only allowed local domain compression which itself is no | ||
| 10 | -longer allowed. | ||
| 11 | - | ||
| 12 | -Other key types inherit from these or the general proscription that | ||
| 13 | -compression is not allow in new types. | ||
| 14 | - | ||
| 15 | -(cherry picked from commit 5e397a16dabd12c6c2459840dcd9db976d950c6c) | ||
| 16 | - | ||
| 17 | -Conflict:no | ||
| 18 | -Reference:https://gitlab.isc.org/isc-projects/bind9/-/commit/9ddc60767d4614a405b8a2c9938fe00cf60b361a | ||
| 19 | ---- | ||
| 20 | - lib/dns/rdata.c | 2 +- | ||
| 21 | - 1 file changed, 1 insertion(+), 1 deletion(-) | ||
| 22 | - | ||
| 23 | -diff --git a/lib/dns/rdata.c b/lib/dns/rdata.c | ||
| 24 | -index 42549ac4c5..bd18777be7 100644 | ||
| 25 | ---- a/lib/dns/rdata.c | ||
| 26 | -+++ b/lib/dns/rdata.c | ||
| 27 | -@@ -591,7 +591,7 @@ check_private(isc_buffer_t *source, dns_secalg_t alg) { | ||
| 28 | - dns_fixedname_t fixed; | ||
| 29 | - | ||
| 30 | - RETERR(dns_name_fromwire(dns_fixedname_initname(&fixed), source, | ||
| 31 | -- DNS_DECOMPRESS_DEFAULT, NULL)); | ||
| 32 | -+ DNS_DECOMPRESS_NEVER, NULL)); | ||
| 33 | - } else if (alg == DNS_KEYALG_PRIVATEOID) { | ||
| 34 | - /* | ||
| 35 | - * Check that we can extract the OID from the start of the | ||
| 36 | --- | ||
| 37 | -2.43.0 | ||
| 38 | - | ||
| @@ -1,50 +0,0 @@ | |||
| 1 | -From 6fc91ef40bebc2b5e98474fa3bad57d04e8b494a Mon Sep 17 00:00:00 2001 | ||
| 2 | -From: Mark Andrews <marka@isc.org> | ||
| 3 | -Date: Tue, 19 May 2026 12:03:23 +1000 | ||
| 4 | -Subject: [PATCH] Check that a short PRIVATEDNS record is rejected | ||
| 5 | - | ||
| 6 | -A bug in dns_name_fromwire meant that short PRIVATEDNS key | ||
| 7 | -records where being accepted. Test that this is no longer | ||
| 8 | -the case. | ||
| 9 | - | ||
| 10 | -(cherry picked from commit f48d48027384d8c2210b5ce9e3eac7af101ead3d) | ||
| 11 | - | ||
| 12 | -Conflict:no | ||
| 13 | -Reference:https://gitlab.isc.org/isc-projects/bind9/-/commit/6fc91ef40bebc2b5e98474fa3bad57d04e8b494a | ||
| 14 | ---- | ||
| 15 | - tests/dns/rdata_test.c | 19 +++++++++++++++++++ | ||
| 16 | - 1 file changed, 19 insertions(+) | ||
| 17 | - | ||
| 18 | -diff --git a/tests/dns/rdata_test.c b/tests/dns/rdata_test.c | ||
| 19 | -index 7af9c56329..13d790b456 100644 | ||
| 20 | ---- a/tests/dns/rdata_test.c | ||
| 21 | -+++ b/tests/dns/rdata_test.c | ||
| 22 | -@@ -2276,6 +2276,25 @@ ISC_RUN_TEST_IMPL(key) { | ||
| 23 | - | ||
| 24 | - check_rdata(text_ok, wire_ok, NULL, false, dns_rdataclass_in, | ||
| 25 | - dns_rdatatype_key, sizeof(dns_rdata_key_t)); | ||
| 26 | -+ | ||
| 27 | -+ /* | ||
| 28 | -+ * A valid PRIVATEDNS record with an active region shorter than the | ||
| 29 | -+ * actual record length. A bug in dns_name_fromwire meant that this | ||
| 30 | -+ * was previously accepted. | ||
| 31 | -+ */ | ||
| 32 | -+ unsigned char key[] = { 0x00, 0x00, 0x00, 253, 0x07, 'e', 'x', | ||
| 33 | -+ 'a', 'm', 'p', 'l', 'e', 0x00 }; | ||
| 34 | -+ unsigned char buf[sizeof(key)]; | ||
| 35 | -+ isc_buffer_t source, target; | ||
| 36 | -+ isc_result_t result; | ||
| 37 | -+ | ||
| 38 | -+ isc_buffer_init(&source, key, sizeof(key)); | ||
| 39 | -+ isc_buffer_add(&source, sizeof(key)); | ||
| 40 | -+ isc_buffer_setactive(&source, sizeof(key) - 1); | ||
| 41 | -+ isc_buffer_init(&target, buf, sizeof(buf)); | ||
| 42 | -+ result = dns_rdata_fromwire(NULL, dns_rdataclass_in, dns_rdatatype_key, | ||
| 43 | -+ &source, DNS_DECOMPRESS_ALWAYS, &target); | ||
| 44 | -+ assert_int_not_equal(result, ISC_R_SUCCESS); | ||
| 45 | - } | ||
| 46 | - | ||
| 47 | - /* | ||
| 48 | --- | ||
| 49 | -2.43.0 | ||
| 50 | - | ||
| @@ -1,120 +0,0 @@ | |||
| 1 | -From 9a82af76e8b7249f512dd50a9a7925270475a22f Mon Sep 17 00:00:00 2001 | ||
| 2 | -From: Mark Andrews <marka@isc.org> | ||
| 3 | -Date: Fri, 5 Jun 2026 09:18:30 +1000 | ||
| 4 | -Subject: [PATCH] POC for PRIVATEDNS DNSKEY overrun not being detected | ||
| 5 | - | ||
| 6 | -Construct a DNS message where a PRIVATEDNS DNSKEY identifier | ||
| 7 | -overruns the record boundary by 3 byte so that the label ends | ||
| 8 | -at the end of the compression pointer for the next record. The | ||
| 9 | -next type is less than 256 so the next octet is 00 terminating | ||
| 10 | -the identifier name. The transfered zone is then written to | ||
| 11 | -disk using master-format text triggering the assertion when the | ||
| 12 | -truncated identier is discovered. | ||
| 13 | - | ||
| 14 | -Note this test will produce a false result in versions of | ||
| 15 | -BIND that do not check the PRIVATEDNS identifier as it looks | ||
| 16 | -for the error message when the transfer is aborted. | ||
| 17 | - | ||
| 18 | -(cherry picked from commit 9ce3bce8bc8b4e9c6a9b1e84b5849c33eb27830e) | ||
| 19 | - | ||
| 20 | -Conflict:context adapte | ||
| 21 | -Reference:https://gitlab.isc.org/isc-projects/bind9/-/commit/9a82af76e8b7249f512dd50a9a7925270475a22f | ||
| 22 | ---- | ||
| 23 | - bin/tests/system/xfer/ans9/ans.py | 31 ++++++++++++++++++++++++- | ||
| 24 | - bin/tests/system/xfer/ns6/named.conf.j2 | 9 +++++++ | ||
| 25 | - bin/tests/system/xfer/tests_xfer.py | 9 +++++++ | ||
| 26 | - 3 files changed, 48 insertions(+), 1 deletion(-) | ||
| 27 | - | ||
| 28 | -diff --git a/bin/tests/system/xfer/ans9/ans.py b/bin/tests/system/xfer/ans9/ans.py | ||
| 29 | -index 25da6b7..a9e7395 100644 | ||
| 30 | ---- a/bin/tests/system/xfer/ans9/ans.py | ||
| 31 | -+++ b/bin/tests/system/xfer/ans9/ans.py | ||
| 32 | -@@ -13,6 +13,7 @@ information regarding copyright ownership. | ||
| 33 | - | ||
| 34 | - from collections.abc import AsyncGenerator | ||
| 35 | - | ||
| 36 | -+import dns.name | ||
| 37 | - import dns.rcode | ||
| 38 | - import dns.rdatatype | ||
| 39 | - import dns.rrset | ||
| 40 | -@@ -33,7 +34,7 @@ class AXFRServer(DomainHandler): | ||
| 41 | - version. | ||
| 42 | - """ | ||
| 43 | - | ||
| 44 | -- domains = ["xfr-and-reconfig"] | ||
| 45 | -+ domains = ["xfr-and-reconfig", "private-dns-overrun"] | ||
| 46 | - | ||
| 47 | - def __init__(self) -> None: | ||
| 48 | - super().__init__() | ||
| 49 | -@@ -97,6 +98,34 @@ class AXFRServer(DomainHandler): | ||
| 50 | - | ||
| 51 | - yield DnsResponseSend(txt_message) | ||
| 52 | - | ||
| 53 | -+ if qctx.qname == dns.name.from_text("private-dns-overrun"): | ||
| 54 | -+ # A message where the malformed DNSKEY algorithm identifier | ||
| 55 | -+ # finishes on a 00 byte in the next record. Assumes the | ||
| 56 | -+ # next record starts with a compression pointer which is | ||
| 57 | -+ # followed by the type which starts with 00. | ||
| 58 | -+ | ||
| 59 | -+ # Generate malformed PRIVATE DNS DNSKEY | ||
| 60 | -+ dnskey_message = qctx.prepare_new_response() | ||
| 61 | -+ dnskey_rrset = dns.rrset.from_text( | ||
| 62 | -+ qctx.qname, | ||
| 63 | -+ 300, | ||
| 64 | -+ qctx.qclass, | ||
| 65 | -+ dns.rdatatype.DNSKEY, | ||
| 66 | -+ "\\# 12 00 00 00 fd 09 00 00 00 00 00 00 00", | ||
| 67 | -+ ) | ||
| 68 | -+ dnskey_message.answer.append(dnskey_rrset) | ||
| 69 | -+ # Generate well formed PRIVATE DNS DNSKEY | ||
| 70 | -+ dnskey_rrset = dns.rrset.from_text( | ||
| 71 | -+ qctx.qname, | ||
| 72 | -+ 300, | ||
| 73 | -+ qctx.qclass, | ||
| 74 | -+ dns.rdatatype.DNSKEY, | ||
| 75 | -+ "\\# 12 00 00 00 fd 06 00 00 00 00 00 00 00", | ||
| 76 | -+ ) | ||
| 77 | -+ dnskey_message.answer.append(dnskey_rrset) | ||
| 78 | -+ | ||
| 79 | -+ yield DnsResponseSend(dnskey_message) | ||
| 80 | -+ | ||
| 81 | - # Finish the AXFR transaction by sending the second SOA RRset. | ||
| 82 | - yield DnsResponseSend(soa_message) | ||
| 83 | - | ||
| 84 | -diff --git a/bin/tests/system/xfer/ns6/named.conf.j2 b/bin/tests/system/xfer/ns6/named.conf.j2 | ||
| 85 | -index 08ca01f..1f2292a 100644 | ||
| 86 | ---- a/bin/tests/system/xfer/ns6/named.conf.j2 | ||
| 87 | -+++ b/bin/tests/system/xfer/ns6/named.conf.j2 | ||
| 88 | -@@ -111,3 +111,12 @@ zone "xfr-and-reconfig" { | ||
| 89 | - file "xfr-and-reconfig.bk"; | ||
| 90 | - request-ixfr no; # ans9 supports only axfr | ||
| 91 | - }; | ||
| 92 | -+ | ||
| 93 | -+# GL#6004 | ||
| 94 | -+zone "private-dns-overrun" { | ||
| 95 | -+ type secondary; | ||
| 96 | -+ primaries { 10.53.0.9; }; | ||
| 97 | -+ file "private-dns-overrun.bk"; | ||
| 98 | -+ masterfile-format text; # force bug to be exercised | ||
| 99 | -+ request-ixfr no; # ans9 supports only axfr | ||
| 100 | -+}; | ||
| 101 | -diff --git a/bin/tests/system/xfer/tests_xfer.py b/bin/tests/system/xfer/tests_xfer.py | ||
| 102 | -index 78b9017..f7b9905 100644 | ||
| 103 | ---- a/bin/tests/system/xfer/tests_xfer.py | ||
| 104 | -+++ b/bin/tests/system/xfer/tests_xfer.py | ||
| 105 | -@@ -549,3 +549,12 @@ def test_reconfiguration_when_zone_transfer_is_in_the_middle_of_soa_query(ns6): | ||
| 106 | - isctest.log.info("Try to reload the zone from the primary") | ||
| 107 | - ns6.rndc("reload xfr-and-reconfig") | ||
| 108 | - watcher_transfer_started.wait_for_line("Transfer started") | ||
| 109 | -+ | ||
| 110 | -+def test_malformed_private_dns_identifier_overrun(ns6): | ||
| 111 | -+ isctest.log.info( | ||
| 112 | -+ "Check that a malformed PRIVATEDNS DNSKEY which overruns the record is rejected" | ||
| 113 | -+ ) | ||
| 114 | -+ with ns6.watch_log_from_start(timeout=60) as watcher_transfer_completed: | ||
| 115 | -+ watcher_transfer_completed.wait_for_line( | ||
| 116 | -+ "zone private-dns-overrun/IN: zone transfer finished: unexpected end of input" | ||
| 117 | -+ ) | ||
| 118 | --- | ||
| 119 | -2.43.0 | ||
| 120 | - | ||
| @@ -1,33 +0,0 @@ | |||
| 1 | -From 4167d965330232d0499e61db0b13d9d0a97e64da Mon Sep 17 00:00:00 2001 | ||
| 2 | -From: Mark Andrews <marka@isc.org> | ||
| 3 | -Date: Fri, 10 Apr 2026 10:26:14 +1000 | ||
| 4 | -Subject: [PATCH] Properly handle rpz name to long wildcard expansion | ||
| 5 | - | ||
| 6 | -Previously a self referential CNAME and the original address | ||
| 7 | -record were returned. We now return a YXDOMAIN response. | ||
| 8 | - | ||
| 9 | -(cherry picked from commit cfc4c4f69870ce492deaaa429453563d1621ded3) | ||
| 10 | - | ||
| 11 | -Conflict:no | ||
| 12 | -Reference:https://gitlab.isc.org/isc-projects/bind9/-/commit/4167d965330232d0499e61db0b13d9d0a97e64da | ||
| 13 | ---- | ||
| 14 | - lib/ns/query.c | 3 ++- | ||
| 15 | - 1 file changed, 2 insertions(+), 1 deletion(-) | ||
| 16 | - | ||
| 17 | -diff --git a/lib/ns/query.c b/lib/ns/query.c | ||
| 18 | -index 0e4396a228..6d31570f3e 100644 | ||
| 19 | ---- a/lib/ns/query.c | ||
| 20 | -+++ b/lib/ns/query.c | ||
| 21 | -@@ -7334,7 +7334,8 @@ query_rpzcname(query_ctx_t *qctx, dns_name_t *cname) { | ||
| 22 | - qctx->fname, NULL); | ||
| 23 | - if (result == DNS_R_NAMETOOLONG) { | ||
| 24 | - client->message->rcode = dns_rcode_yxdomain; | ||
| 25 | -- } else if (result != ISC_R_SUCCESS) { | ||
| 26 | -+ } | ||
| 27 | -+ if (result != ISC_R_SUCCESS) { | ||
| 28 | - return result; | ||
| 29 | - } | ||
| 30 | - } else { | ||
| 31 | --- | ||
| 32 | -2.43.0 | ||
| 33 | - | ||
| @@ -1,29 +0,0 @@ | |||
| 1 | -From c392d9958b02d6b82c5566f5d499d87a1297b854 Mon Sep 17 00:00:00 2001 | ||
| 2 | -From: Mark Andrews <marka@isc.org> | ||
| 3 | -Date: Fri, 10 Apr 2026 10:24:06 +1000 | ||
| 4 | -Subject: [PATCH] Fix TTL extraction from A/AAAA record | ||
| 5 | - | ||
| 6 | -(cherry picked from commit 89c86e338db2492b92e6618c586f146c6928dc6d) | ||
| 7 | - | ||
| 8 | -Conflict:no | ||
| 9 | -Reference:https://gitlab.isc.org/isc-projects/bind9/-/commit/c392d9958b02d6b82c5566f5d499d87a1297b854 | ||
| 10 | ---- | ||
| 11 | - bin/tests/system/rpz/tests.sh | 2 +- | ||
| 12 | - 1 file changed, 1 insertion(+), 1 deletion(-) | ||
| 13 | - | ||
| 14 | -diff --git a/bin/tests/system/rpz/tests.sh b/bin/tests/system/rpz/tests.sh | ||
| 15 | -index f15215a43c..bdcc06de53 100644 | ||
| 16 | ---- a/bin/tests/system/rpz/tests.sh | ||
| 17 | -+++ b/bin/tests/system/rpz/tests.sh | ||
| 18 | -@@ -419,7 +419,7 @@ addr() { | ||
| 19 | - digcmd $2 >$DIGNM | ||
| 20 | - #ckalive "$2" "server crashed by 'dig $2'" || return 1 | ||
| 21 | - ADDR_ESC=$(echo "$ADDR" | sed -e 's/\./\\./g') | ||
| 22 | -- ADDR_TTL=$(sed -n -e "s/^[-.a-z0-9]\{1,\}[ ]*\([0-9]*\) IN AA* ${ADDR_ESC}\$/\1/p" $DIGNM) | ||
| 23 | -+ ADDR_TTL=$(sed -n -e "s/^[-.a-z0-9]\{1,\}[ ]*\([0-9]*\)[ ]IN[ ]AA*[ ]${ADDR_ESC}\$/\1/p" $DIGNM) | ||
| 24 | - if test -z "$ADDR_TTL"; then | ||
| 25 | - setret "'dig $2' wrong; no address $ADDR record in $DIGNM" | ||
| 26 | - return 0 | ||
| 27 | --- | ||
| 28 | -2.43.0 | ||
| 29 | - | ||
| @@ -1,57 +0,0 @@ | |||
| 1 | -From 1009cae26631efdbeab1a452462d12b73eef69f4 Mon Sep 17 00:00:00 2001 | ||
| 2 | -From: Mark Andrews <marka@isc.org> | ||
| 3 | -Date: Fri, 10 Apr 2026 10:24:40 +1000 | ||
| 4 | -Subject: [PATCH] Check rpz name too long wildcard CNAME expansion handling | ||
| 5 | - | ||
| 6 | -(cherry picked from commit 9345394e2097031b55b3ef34ceaadf5a7ebbeef2) | ||
| 7 | - | ||
| 8 | -Conflict:no | ||
| 9 | -Reference:https://gitlab.isc.org/isc-projects/bind9/-/commit/1009cae26631efdbeab1a452462d12b73eef69f4 | ||
| 10 | ---- | ||
| 11 | - bin/tests/system/rpz/ns2/tld2.db | 2 ++ | ||
| 12 | - bin/tests/system/rpz/ns4/tld4.db | 2 ++ | ||
| 13 | - bin/tests/system/rpz/tests.sh | 5 ++++- | ||
| 14 | - 3 files changed, 8 insertions(+), 1 deletion(-) | ||
| 15 | - | ||
| 16 | -diff --git a/bin/tests/system/rpz/ns2/tld2.db b/bin/tests/system/rpz/ns2/tld2.db | ||
| 17 | -index a66ee16d14..fbc7b8318a 100644 | ||
| 18 | ---- a/bin/tests/system/rpz/ns2/tld2.db | ||
| 19 | -+++ b/bin/tests/system/rpz/ns2/tld2.db | ||
| 20 | -@@ -126,3 +126,5 @@ a7-2 A 192.168.7.2 | ||
| 21 | - | ||
| 22 | - a8-1 A 192.168.8.1 | ||
| 23 | - TXT "a8-1 tld2 text" | ||
| 24 | -+ | ||
| 25 | -+*.wild A 192.168.9.1 | ||
| 26 | -diff --git a/bin/tests/system/rpz/ns4/tld4.db b/bin/tests/system/rpz/ns4/tld4.db | ||
| 27 | -index e65dd94a33..d6b5d98946 100644 | ||
| 28 | ---- a/bin/tests/system/rpz/ns4/tld4.db | ||
| 29 | -+++ b/bin/tests/system/rpz/ns4/tld4.db | ||
| 30 | -@@ -59,6 +59,8 @@ a3-6.tld2 A 56.56.56.56 | ||
| 31 | - | ||
| 32 | - a3-7.sub1.tld2 A 57.57.57.57 | ||
| 33 | - | ||
| 34 | -+*.wild.sub1.tld2 A 57.57.57.57 | ||
| 35 | -+ | ||
| 36 | - a3-8.tld2 A 58.58.58.58 | ||
| 37 | - | ||
| 38 | - a3-9.sub9.tld2 A 59.59.59.59 | ||
| 39 | -diff --git a/bin/tests/system/rpz/tests.sh b/bin/tests/system/rpz/tests.sh | ||
| 40 | -index bdcc06de53..ca704e11e2 100644 | ||
| 41 | ---- a/bin/tests/system/rpz/tests.sh | ||
| 42 | -+++ b/bin/tests/system/rpz/tests.sh | ||
| 43 | -@@ -545,7 +545,10 @@ nochange TCP a3-9.tld2 # 33 tcp-only | ||
| 44 | - here x.servfail <<'EOF' # 34 qname-wait-recurse yes | ||
| 45 | - ;; status: SERVFAIL, x | ||
| 46 | - EOF | ||
| 47 | --addr 35.35.35.35 "x.servfail @$ns5" # 35 qname-wait-recurse no | ||
| 48 | -+addr 35.35.35.35 "x.servfail @$ns5" # 35 qname-wait-recurse no | ||
| 49 | -+here aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.wild.sub1.tld2 <<'EOF' # 36 wildcard CNAME name to long | ||
| 50 | -+ ;; status: YXDOMAIN, x | ||
| 51 | -+EOF | ||
| 52 | - end_group | ||
| 53 | - ckstats $ns3 test1 ns3 22 | ||
| 54 | - ckstats $ns5 test1 ns5 1 | ||
| 55 | --- | ||
| 56 | -2.43.0 | ||
| 57 | - | ||
| @@ -1,42 +0,0 @@ | |||
| 1 | -From e33d61bf7494a9a9399a7111f0b3b7f44fb9e749 Mon Sep 17 00:00:00 2001 | ||
| 2 | -From: =?UTF-8?q?Ond=C5=99ej=20Sur=C3=BD?= <ondrej@sury.org> | ||
| 3 | -Date: Sun, 7 Jun 2026 10:19:44 +0200 | ||
| 4 | -Subject: [PATCH] Limit DNSSEC denial proof validation per fetch | ||
| 5 | - | ||
| 6 | -Negative response validation reuses the per-fetch validation counters, | ||
| 7 | -but NSEC/NSEC3 subvalidator quota failures were not terminal. The | ||
| 8 | -parent validator logged the child failure and resumed validate_nx(), | ||
| 9 | -so a response stuffed with many denial proof RRsets could keep driving | ||
| 10 | -more validation work and still eventually validate. | ||
| 11 | - | ||
| 12 | -Treat ISC_R_QUOTA from an NSEC/NSEC3 subvalidator as terminal and | ||
| 13 | -propagate the quota reason to the parent validator. This lets the | ||
| 14 | -existing fctx->nvalidations and fctx->nfails limits stop excessive | ||
| 15 | -negative proof validation without a separate resolver-side pre-scan. | ||
| 16 | - | ||
| 17 | -(cherry picked from commit c3388378a7699c956603bc405f73e38d2c344dc8) | ||
| 18 | - | ||
| 19 | -Conflict:no | ||
| 20 | -Reference:https://gitlab.isc.org/isc-projects/bind9/-/commit/e33d61bf7494a9a9399a7111f0b3b7f44fb9e749 | ||
| 21 | ---- | ||
| 22 | - lib/dns/validator.c | 4 ++++ | ||
| 23 | - 1 file changed, 4 insertions(+) | ||
| 24 | - | ||
| 25 | -diff --git a/lib/dns/validator.c b/lib/dns/validator.c | ||
| 26 | -index 84fbfd5789..69176462e3 100644 | ||
| 27 | ---- a/lib/dns/validator.c | ||
| 28 | -+++ b/lib/dns/validator.c | ||
| 29 | -@@ -911,6 +911,10 @@ validator_callback_nsec(void *arg) { | ||
| 30 | - switch (eresult) { | ||
| 31 | - case ISC_R_CANCELED: | ||
| 32 | - case ISC_R_SHUTTINGDOWN: | ||
| 33 | -+ case ISC_R_QUOTA: | ||
| 34 | -+ val->attributes |= subvalidator->attributes & | ||
| 35 | -+ (VALATTR_MAXVALIDATIONS | | ||
| 36 | -+ VALATTR_MAXVALIDATIONFAILS); | ||
| 37 | - result = eresult; | ||
| 38 | - break; | ||
| 39 | - case DNS_R_BROKENCHAIN: | ||
| 40 | --- | ||
| 41 | -2.43.0 | ||
| 42 | - | ||
| @@ -1,623 +0,0 @@ | |||
| 1 | -From 2dabf117e1264fd13fb33096f87e78a039fd1c6c Mon Sep 17 00:00:00 2001 | ||
| 2 | -From: =?UTF-8?q?Ond=C5=99ej=20Sur=C3=BD?= <ondrej@sury.org> | ||
| 3 | -Date: Tue, 23 Jun 2026 10:59:38 +0200 | ||
| 4 | -Subject: [PATCH] Make the dns_slabheaders in the cache reference counted | ||
| 5 | - | ||
| 6 | -Instead of only reference counting the enclosing qpcnode, add the | ||
| 7 | -reference counting directly to the slabheaders. The reference is | ||
| 8 | -incremented when an rdataset is bound to the header and decremented when | ||
| 9 | -the rdataset is disassociated, so a stale slabheader can be removed from | ||
| 10 | -the node's down chain as soon as its own reference count reaches zero, | ||
| 11 | -instead of waiting for the whole qpcnode to become unreferenced. | ||
| 12 | - | ||
| 13 | -Building on that, clean up the ancient headers eagerly: mark_ancient() | ||
| 14 | -is made idempotent, releases the header's own (container) reference and | ||
| 15 | -reaps the stale headers from the node's down chain as soon as their | ||
| 16 | -references reach zero. A header evicted over the per-name type limit is | ||
| 17 | -expired only after the new rdataset has been bound, so the bind's | ||
| 18 | -increment always precedes mark_ancient()'s decrement. | ||
| 19 | - | ||
| 20 | -Because a header can now be reclaimed independently of its node, the | ||
| 21 | -rdataset iterators must keep the header they are positioned on alive: | ||
| 22 | -each iterator takes a reference on its current header and releases it | ||
| 23 | -when it advances or is destroyed. Iteration otherwise stays lazy and | ||
| 24 | -re-reads the node on every step, so it still observes records added to | ||
| 25 | -the node while the iterator is live, as zone signing requires. | ||
| 26 | - | ||
| 27 | -The slab headers are shared with the zone databases, so the matching | ||
| 28 | -increment is added to every bind path. The noqname/closest proofs hand | ||
| 29 | -out rdatasets backed by bare slabs that have no header, so they are | ||
| 30 | -given a separate dns_rdataproof_rdatasetmethods that leaves the | ||
| 31 | -reference count untouched. | ||
| 32 | - | ||
| 33 | -Conflict:no | ||
| 34 | -Reference:https://gitlab.isc.org/isc-projects/bind9/-/commit/2dabf117e1264fd13fb33096f87e78a039fd1c6c | ||
| 35 | ---- | ||
| 36 | - bin/tests/system/reclimit/tests.sh | 4 +- | ||
| 37 | - lib/dns/include/dns/rdataslab.h | 3 ++ | ||
| 38 | - lib/dns/qpcache.c | 67 ++++++++++++++++++++++------ | ||
| 39 | - lib/dns/qpzone.c | 25 +++++++++++ | ||
| 40 | - lib/dns/rbt-cachedb.c | 16 ++----- | ||
| 41 | - lib/dns/rbtdb.c | 71 +++++++++++++++++++++++------- | ||
| 42 | - lib/dns/rbtdb_p.h | 2 + | ||
| 43 | - lib/dns/rdataslab.c | 35 +++++++++++++-- | ||
| 44 | - 8 files changed, 174 insertions(+), 49 deletions(-) | ||
| 45 | - | ||
| 46 | -diff --git a/bin/tests/system/reclimit/tests.sh b/bin/tests/system/reclimit/tests.sh | ||
| 47 | -index e7510886cb..698251090a 100644 | ||
| 48 | ---- a/bin/tests/system/reclimit/tests.sh | ||
| 49 | -+++ b/bin/tests/system/reclimit/tests.sh | ||
| 50 | -@@ -341,13 +341,13 @@ echo_i "checking that NXDOMAIN names over the max-types-per-name limit don't get | ||
| 51 | - | ||
| 52 | - # Query for 10 NXDOMAIN types | ||
| 53 | - for ntype in $(seq 65270 65279); do | ||
| 54 | -- check_manytypes 1 manytypes.big "TYPE${ntype}" NOERROR big SOA 0 || ret=1 | ||
| 55 | -+ check_manytypes 1 manytypes.big "TYPE${ntype}" NOERROR big SOA 120 || ret=1 | ||
| 56 | - done | ||
| 57 | - # Wait at least 1 second | ||
| 58 | - sleep 1 | ||
| 59 | - # Query for 10 NXDOMAIN types again - these should not be cached | ||
| 60 | - for ntype in $(seq 65270 65279); do | ||
| 61 | -- check_manytypes 2 manytypes.big "TYPE${ntype}" NOERROR big SOA 0 || ret=1 | ||
| 62 | -+ check_manytypes 2 manytypes.big "TYPE${ntype}" NOERROR big SOA 120 || ret=1 | ||
| 63 | - done | ||
| 64 | - | ||
| 65 | - if [ $ret -ne 0 ]; then echo_i "failed"; fi | ||
| 66 | -diff --git a/lib/dns/include/dns/rdataslab.h b/lib/dns/include/dns/rdataslab.h | ||
| 67 | -index 8fc68322ca..6eeb55d24a 100644 | ||
| 68 | ---- a/lib/dns/include/dns/rdataslab.h | ||
| 69 | -+++ b/lib/dns/include/dns/rdataslab.h | ||
| 70 | -@@ -46,6 +46,7 @@ | ||
| 71 | - #include <isc/atomic.h> | ||
| 72 | - #include <isc/heap.h> | ||
| 73 | - #include <isc/lang.h> | ||
| 74 | -+#include <isc/refcount.h> | ||
| 75 | - #include <isc/stdtime.h> | ||
| 76 | - #include <isc/urcu.h> | ||
| 77 | - | ||
| 78 | -@@ -95,6 +96,8 @@ struct dns_slabheader { | ||
| 79 | - isc_stdtime_t last_used; | ||
| 80 | - _Atomic(uint32_t) last_refresh_fail_ts; | ||
| 81 | - | ||
| 82 | -+ isc_refcount_t references; | ||
| 83 | -+ | ||
| 84 | - dns_slabheader_proof_t *noqname; | ||
| 85 | - dns_slabheader_proof_t *closest; | ||
| 86 | - /*%< | ||
| 87 | -diff --git a/lib/dns/qpcache.c b/lib/dns/qpcache.c | ||
| 88 | -index b0ff0f50d5..7295e47e33 100644 | ||
| 89 | ---- a/lib/dns/qpcache.c | ||
| 90 | -+++ b/lib/dns/qpcache.c | ||
| 91 | -@@ -592,13 +592,19 @@ update_header(qpcache_t *qpdb, dns_slabheader_t *header, isc_stdtime_t now) { | ||
| 92 | - | ||
| 93 | - static void | ||
| 94 | - clean_stale_headers(dns_slabheader_t *top) { | ||
| 95 | -- dns_slabheader_t *d = NULL, *down_next = NULL; | ||
| 96 | -+ dns_slabheader_t *d = NULL, *down_next = NULL, *down_parent = top; | ||
| 97 | - | ||
| 98 | - for (d = top->down; d != NULL; d = down_next) { | ||
| 99 | - down_next = d->down; | ||
| 100 | -- dns_slabheader_destroy(&d); | ||
| @@ -1,35 +0,0 @@ | |||
| 1 | -From dc1436fa28315a27b5185e4163cd0fa877bda3e4 Mon Sep 17 00:00:00 2001 | ||
| 2 | -From: Mark Andrews <marka@isc.org> | ||
| 3 | -Date: Tue, 14 Apr 2026 15:14:06 +1000 | ||
| 4 | -Subject: [PATCH] Don't sign out of zone records in dnssec-signzone | ||
| 5 | - | ||
| 6 | -dnssec-signzone was signing extraneous records that were not within | ||
| 7 | -the namespace of the zone. This no longer occurs. | ||
| 8 | - | ||
| 9 | -(cherry picked from commit e45c9af7051421fd370f20ba8325199c606223fd) | ||
| 10 | - | ||
| 11 | -Conflict:no | ||
| 12 | -Reference:https://gitlab.isc.org/isc-projects/bind9/-/commit/dc1436fa28315a27b5185e4163cd0fa877bda3e4 | ||
| 13 | ---- | ||
| 14 | - bin/dnssec/dnssec-signzone.c | 5 +++++ | ||
| 15 | - 1 file changed, 5 insertions(+) | ||
| 16 | - | ||
| 17 | -diff --git a/bin/dnssec/dnssec-signzone.c b/bin/dnssec/dnssec-signzone.c | ||
| 18 | -index c7c0798088..63234040d4 100644 | ||
| 19 | ---- a/bin/dnssec/dnssec-signzone.c | ||
| 20 | -+++ b/bin/dnssec/dnssec-signzone.c | ||
| 21 | -@@ -1623,6 +1623,11 @@ assignwork(void *arg) { | ||
| 22 | - dns_db_detachnode(gdb, &node); | ||
| 23 | - goto next; | ||
| 24 | - } | ||
| 25 | -+ if (!dns_name_issubdomain(name, gorigin)) { | ||
| 26 | -+ dumpnode(name, node); | ||
| 27 | -+ dns_db_detachnode(gdb, &node); | ||
| 28 | -+ goto next; | ||
| 29 | -+ } | ||
| 30 | - /* | ||
| 31 | - * Sort the zone data from the glue and out-of-zone data. | ||
| 32 | - * For NSEC zones nodes with zone data have NSEC records. | ||
| 33 | --- | ||
| 34 | -2.43.0 | ||
| 35 | - | ||
| @@ -1,219 +0,0 @@ | |||
| 1 | -From 8a46533cbeab78c215ee035d73967a3a59925375 Mon Sep 17 00:00:00 2001 | ||
| 2 | -From: Mark Andrews <marka@isc.org> | ||
| 3 | -Date: Tue, 14 Apr 2026 12:24:33 +1000 | ||
| 4 | -Subject: [PATCH] Invalid signed wildcard records were being accepted | ||
| 5 | - | ||
| 6 | -An RRSIG whose Labels field indicates fewer labels than its signer | ||
| 7 | -name requires was being accepted. When such a record covers a | ||
| 8 | -wildcard, the validator reconstructs a wildcard owner name above the | ||
| 9 | -signer's zone and caches it as secure. RFC 8198 cache synthesis | ||
| 10 | -(synth-from-dnssec) then serves that forged wildcard for unrelated | ||
| 11 | -names, poisoning the cache. | ||
| 12 | - | ||
| 13 | -These records are now rejected, both when an RRSIG is parsed and when | ||
| 14 | -its signature is verified. | ||
| 15 | - | ||
| 16 | -(cherry picked from commit 084ca5ee10515e461d46b63df9660b8394bc7de9) | ||
| 17 | - | ||
| 18 | -Conflict:no | ||
| 19 | -Reference:https://gitlab.isc.org/isc-projects/bind9/-/commit/8a46533cbeab78c215ee035d73967a3a59925375 | ||
| 20 | ---- | ||
| 21 | - lib/dns/dnssec.c | 39 ++++++++++++++++++++++++-------- | ||
| 22 | - lib/dns/rdata/generic/rrsig_46.c | 32 ++++++++++++++++++++------ | ||
| 23 | - 2 files changed, 54 insertions(+), 17 deletions(-) | ||
| 24 | - | ||
| 25 | -diff --git a/lib/dns/dnssec.c b/lib/dns/dnssec.c | ||
| 26 | -index aa664b9187..4b5ddc2184 100644 | ||
| 27 | ---- a/lib/dns/dnssec.c | ||
| 28 | -+++ b/lib/dns/dnssec.c | ||
| 29 | -@@ -129,7 +129,7 @@ dns_dnssec_keyfromrdata(const dns_name_t *name, const dns_rdata_t *rdata, | ||
| 30 | - isc_buffer_t b; | ||
| 31 | - isc_region_t r; | ||
| 32 | - | ||
| 33 | -- INSIST(name != NULL); | ||
| 34 | -+ INSIST(DNS_NAME_VALID(name)); | ||
| 35 | - INSIST(rdata != NULL); | ||
| 36 | - INSIST(mctx != NULL); | ||
| 37 | - INSIST(key != NULL); | ||
| 38 | -@@ -186,12 +186,14 @@ dns_dnssec_sign(const dns_name_t *name, dns_rdataset_t *set, dst_key_t *key, | ||
| 39 | - isc_result_t ret; | ||
| 40 | - isc_buffer_t *databuf = NULL; | ||
| 41 | - char data[256 + 8]; | ||
| 42 | -+ unsigned int labels; | ||
| 43 | - unsigned int sigsize; | ||
| 44 | - dns_fixedname_t fnewname; | ||
| 45 | - dns_fixedname_t fsigner; | ||
| 46 | - | ||
| 47 | -- REQUIRE(name != NULL); | ||
| 48 | -- REQUIRE(dns_name_countlabels(name) <= 255); | ||
| 49 | -+ REQUIRE(DNS_NAME_VALID(name)); | ||
| 50 | -+ labels = dns_name_countlabels(name); | ||
| 51 | -+ REQUIRE(labels <= 255 && labels > 0); | ||
| 52 | - REQUIRE(set != NULL); | ||
| 53 | - REQUIRE(key != NULL); | ||
| 54 | - REQUIRE(inception != NULL); | ||
| 55 | -@@ -220,7 +222,7 @@ dns_dnssec_sign(const dns_name_t *name, dns_rdataset_t *set, dst_key_t *key, | ||
| 56 | - | ||
| 57 | - sig.covered = set->type; | ||
| 58 | - sig.algorithm = dst_key_alg(key); | ||
| 59 | -- sig.labels = dns_name_countlabels(name) - 1; | ||
| 60 | -+ sig.labels = labels - 1; | ||
| 61 | - if (dns_name_iswildcard(name)) { | ||
| 62 | - sig.labels--; | ||
| 63 | - } | ||
| 64 | -@@ -364,10 +366,13 @@ dns_dnssec_verify(const dns_name_t *name, dns_rdataset_t *set, dst_key_t *key, | ||
| 65 | - isc_result_t ret; | ||
| 66 | - unsigned char data[300]; | ||
| 67 | - dst_context_t *ctx = NULL; | ||
| 68 | -- int labels = 0; | ||
| 69 | -+ unsigned int labels; | ||
| 70 | -+ unsigned int siglabels; | ||
| 71 | - bool downcase = false; | ||
| 72 | - | ||
| 73 | -- REQUIRE(name != NULL); | ||
| 74 | -+ REQUIRE(DNS_NAME_VALID(name)); | ||
| 75 | -+ labels = dns_name_countlabels(name); | ||
| 76 | -+ REQUIRE(labels > 0); | ||
| 77 | - REQUIRE(set != NULL); | ||
| 78 | - REQUIRE(key != NULL); | ||
| 79 | - REQUIRE(mctx != NULL); | ||
| 80 | -@@ -382,6 +387,21 @@ dns_dnssec_verify(const dns_name_t *name, dns_rdataset_t *set, dst_key_t *key, | ||
| 81 | - return DNS_R_SIGINVALID; | ||
| 82 | - } | ||
| 83 | - | ||
| 84 | -+ /* | ||
| 85 | -+ * The RRSIG labels field can't indicate fewer labels than the | ||
| 86 | -+ * signer. Also the labels shouldn't be greater than that of | ||
| 87 | -+ * the owner name. | ||
| 88 | -+ * | ||
| 89 | -+ * sig.labels doesn't include the root label, so add 1 to account | ||
| 90 | -+ * for it. | ||
| 91 | -+ */ | ||
| 92 | -+ siglabels = sig.labels + 1; | ||
| 93 | -+ if (siglabels < dns_name_countlabels(&sig.signer) || siglabels > labels) | ||
| 94 | -+ { | ||
| 95 | -+ inc_stat(dns_dnssecstats_fail); | ||
| 96 | -+ return DNS_R_SIGINVALID; | ||
| 97 | -+ } | ||
| 98 | -+ | ||
| 99 | - if (isc_serial_lt(sig.timeexpire, sig.timesigned)) { | ||
| 100 | - inc_stat(dns_dnssecstats_fail); | ||
| 101 | - return DNS_R_SIGINVALID; | ||
| 102 | -@@ -448,10 +468,9 @@ again: | ||
| 103 | - * If the name is an expanded wildcard, use the wildcard name. | ||
| 104 | - */ | ||
| 105 | - dns_fixedname_init(&fnewname); | ||
| 106 | -- labels = dns_name_countlabels(name) - 1; | ||
| 107 | - RUNTIME_CHECK(dns_name_downcase(name, dns_fixedname_name(&fnewname), | ||
| 108 | - NULL) == ISC_R_SUCCESS); | ||
| 109 | -- if (labels - sig.labels > 0) { | ||
| 110 | -+ if (labels > siglabels) { | ||
| 111 | - dns_name_split(dns_fixedname_name(&fnewname), sig.labels + 1, | ||
| 112 | - NULL, dns_fixedname_name(&fnewname)); | ||
| 113 | - } | ||
| 114 | -@@ -462,7 +481,7 @@ again: | ||
| 115 | - * Create an envelope for each rdata: <name|type|class|ttl>. | ||
| 116 | - */ | ||
| 117 | - isc_buffer_init(&envbuf, data, sizeof(data)); | ||
| 118 | -- if (labels - sig.labels > 0) { | ||
| 119 | -+ if (labels > siglabels) { | ||
| 120 | - isc_buffer_putuint8(&envbuf, 1); | ||
| 121 | - isc_buffer_putuint8(&envbuf, '*'); | ||
| 122 | - memmove(data + 2, r.base, r.length); | ||
| 123 | -@@ -557,7 +576,7 @@ cleanup_struct: | ||
| 124 | - inc_stat(dns_dnssecstats_fail); | ||
| 125 | - } | ||
| 126 | - | ||
| 127 | -- if (ret == ISC_R_SUCCESS && labels - sig.labels > 0) { | ||
| 128 | -+ if (ret == ISC_R_SUCCESS && labels > siglabels) { | ||
| 129 | - if (wild != NULL) { | ||
| 130 | - RUNTIME_CHECK(dns_name_concatenate( | ||
| 131 | - dns_wildcardname, | ||
| 132 | -diff --git a/lib/dns/rdata/generic/rrsig_46.c b/lib/dns/rdata/generic/rrsig_46.c | ||
| 133 | -index 80b6fbc711..75ea2e8e90 100644 | ||
| 134 | ---- a/lib/dns/rdata/generic/rrsig_46.c | ||
| 135 | -+++ b/lib/dns/rdata/generic/rrsig_46.c | ||
| 136 | -@@ -23,12 +23,12 @@ | ||
| 137 | - static isc_result_t | ||
| 138 | - fromtext_rrsig(ARGS_FROMTEXT) { | ||
| 139 | - isc_token_t token; | ||
| 140 | -- unsigned char alg, c; | ||
| 141 | -+ unsigned char alg, labels; | ||
| 142 | - long i; | ||
| 143 | - dns_rdatatype_t covered; | ||
| 144 | -- char *e; | ||
| 145 | -+ char *e = NULL; | ||
| 146 | - isc_result_t result; | ||
| 147 | -- dns_name_t name; | ||
| 148 | -+ dns_name_t signer; | ||
| 149 | - isc_buffer_t buffer; | ||
| 150 | - uint32_t time_signed, time_expire; | ||
| 151 | - unsigned int used; | ||
| 152 | -@@ -73,8 +73,8 @@ fromtext_rrsig(ARGS_FROMTEXT) { | ||
| 153 | - if (token.value.as_ulong > 0xffU) { | ||
| 154 | - RETTOK(ISC_R_RANGE); | ||
| 155 | - } | ||
| 156 | -- c = (unsigned char)token.value.as_ulong; | ||
| 157 | -- RETERR(mem_tobuffer(target, &c, 1)); | ||
| 158 | -+ labels = (unsigned char)token.value.as_ulong; | ||
| 159 | -+ RETERR(mem_tobuffer(target, &labels, 1)); | ||
| 160 | - | ||
| 161 | - /* | ||
| 162 | - * Original ttl. | ||
| 163 | -@@ -145,12 +145,20 @@ fromtext_rrsig(ARGS_FROMTEXT) { | ||
| 164 | - */ | ||
| 165 | - RETERR(isc_lex_getmastertoken(lexer, &token, isc_tokentype_string, | ||
| 166 | - false)); | ||
| 167 | -- dns_name_init(&name, NULL); | ||
| 168 | -+ dns_name_init(&signer, NULL); | ||
| 169 | - buffer_fromregion(&buffer, &token.value.as_region); | ||
| 170 | - if (origin == NULL) { | ||
| 171 | - origin = dns_rootname; | ||
| 172 | - } | ||
| 173 | -- RETTOK(dns_name_fromtext(&name, &buffer, origin, options, target)); | ||
| 174 | -+ RETTOK(dns_name_fromtext(&signer, &buffer, origin, options, target)); | ||
| 175 | -+ | ||
| 176 | -+ /* | ||
| 177 | -+ * (RRSIG labels doesn't include the root label, so add one | ||
| 178 | -+ * to normalize it before checking against the signer.) | ||
| 179 | -+ */ | ||
| 180 | -+ if ((unsigned int)(labels + 1) < dns_name_countlabels(&signer)) { | ||
| 181 | -+ RETTOK(ISC_R_RANGE); | ||
| 182 | -+ } | ||
| 183 | - | ||
| 184 | - /* | ||
| 185 | - * Sig. | ||
| 186 | -@@ -297,6 +305,7 @@ fromwire_rrsig(ARGS_FROMWIRE) { | ||
| 187 | - isc_region_t sr; | ||
| 188 | - dns_name_t name; | ||
| 189 | - unsigned char algorithm; | ||
| 190 | -+ unsigned char labels; | ||
| 191 | - | ||
| 192 | - REQUIRE(type == dns_rdatatype_rrsig); | ||
| 193 | - | ||
| 194 | -@@ -320,6 +329,7 @@ fromwire_rrsig(ARGS_FROMWIRE) { | ||
| 195 | - } | ||
| 196 | - | ||
| 197 | - algorithm = sr.base[2]; | ||
| 198 | -+ labels = sr.base[3]; | ||
| 199 | - | ||
| 200 | - isc_buffer_forward(source, 18); | ||
| 201 | - RETERR(mem_tobuffer(target, sr.base, 18)); | ||
| 202 | -@@ -330,6 +340,14 @@ fromwire_rrsig(ARGS_FROMWIRE) { | ||
| 203 | - dns_name_init(&name, NULL); | ||
| 204 | - RETERR(dns_name_fromwire(&name, source, dctx, target)); | ||
| 205 | - | ||
| 206 | -+ /* | ||
| 207 | -+ * (RRSIG labels doesn't include the root label, so add one | ||
| 208 | -+ * to normalize it before checking against the signer.) | ||
| 209 | -+ */ | ||
| 210 | -+ if ((unsigned int)(labels + 1) < dns_name_countlabels(&name)) { | ||
| 211 | -+ RETERR(DNS_R_FORMERR); | ||
| 212 | -+ } | ||
| 213 | -+ | ||
| 214 | - /* | ||
| 215 | - * Sig. | ||
| 216 | - */ | ||
| 217 | --- | ||
| 218 | -2.43.0 | ||
| 219 | - | ||
| @@ -1,146 +0,0 @@ | |||
| 1 | -From 34e69230a035287195a1fd4413cf4901eb0e2ee2 Mon Sep 17 00:00:00 2001 | ||
| 2 | -From: Mark Andrews <marka@isc.org> | ||
| 3 | -Date: Tue, 14 Apr 2026 13:46:22 +1000 | ||
| 4 | -Subject: [PATCH] Test RRSIG record parsing | ||
| 5 | - | ||
| 6 | -In particular test that labels and signer fields are consistent. | ||
| 7 | - | ||
| 8 | -(cherry picked from commit 5a95e64731afe63d348d272cc4d3b2f9847150c2) | ||
| 9 | - | ||
| 10 | -Conflict:no | ||
| 11 | -Reference:https://gitlab.isc.org/isc-projects/bind9/-/commit/34e69230a035287195a1fd4413cf4901eb0e2ee2 | ||
| 12 | ---- | ||
| 13 | - tests/dns/rdata_test.c | 110 +++++++++++++++++++++++++++++++++++++++++ | ||
| 14 | - 1 file changed, 110 insertions(+) | ||
| 15 | - | ||
| 16 | -diff --git a/tests/dns/rdata_test.c b/tests/dns/rdata_test.c | ||
| 17 | -index 13d790b456..9938f5a816 100644 | ||
| 18 | ---- a/tests/dns/rdata_test.c | ||
| 19 | -+++ b/tests/dns/rdata_test.c | ||
| 20 | -@@ -2676,6 +2676,115 @@ ISC_RUN_TEST_IMPL(rkey) { | ||
| 21 | - dns_rdatatype_rkey, sizeof(dns_rdata_rkey_t)); | ||
| 22 | - } | ||
| 23 | - | ||
| 24 | -+ISC_RUN_TEST_IMPL(rrsig) { | ||
| 25 | -+ text_ok_t text_ok[] = { | ||
| 26 | -+ TEXT_VALID("SOA 8 0 86400 20260426170000 20260413160000 54393 " | ||
| 27 | -+ ". " | ||
| 28 | -+ "tFbcoVP8MnpecUquJ/aj+XeNgV7ts9GSHVkXaXRJrJ/" | ||
| 29 | -+ "TEkOZApVG0F6E " | ||
| 30 | -+ "9sYpxGk2ItweLL43ujioGj0HWwZDRR+vbur+O/" | ||
| 31 | -+ "dIdheiig1VvU+9HXLi " | ||
| 32 | -+ "QOViY9Kc64ixdyJhYCC5K+bO1qsHxd+" | ||
| 33 | -+ "KJXOaxyHbqchYkDFy4PL6qftE " | ||
| 34 | -+ "VaLkueRgjXgOsq/" | ||
| 35 | -+ "NxvCXDgAa5xy0+3Sl0myxIs8rJ5KeXfJQFe7qxgaw " | ||
| 36 | -+ "VjJsJTKw8neOTw2rQfLaigWu2LIWw+" | ||
| 37 | -+ "IyVrLjZJdLqGkiLBGd1w4X3U12 " | ||
| 38 | -+ "fFxoY3eqzNgBEtduoGKPZ/" | ||
| 39 | -+ "NpP9cuKJORJ18283aV8hR4WO91VR0q1zcM jLwqUg=="), | ||
| 40 | -+ /* labels too short for signer */ | ||
| 41 | -+ TEXT_INVALID("SOA 8 0 86400 20260426170000 20260413160000 " | ||
| 42 | -+ "54393 example. " | ||
| 43 | -+ "tFbcoVP8MnpecUquJ/aj+XeNgV7ts9GSHVkXaXRJrJ/" | ||
| 44 | -+ "TEkOZApVG0F6E " | ||
| 45 | -+ "9sYpxGk2ItweLL43ujioGj0HWwZDRR+vbur+O/" | ||
| 46 | -+ "dIdheiig1VvU+9HXLi " | ||
| 47 | -+ "QOViY9Kc64ixdyJhYCC5K+bO1qsHxd+" | ||
| 48 | -+ "KJXOaxyHbqchYkDFy4PL6qftE " | ||
| 49 | -+ "VaLkueRgjXgOsq/" | ||
| 50 | -+ "NxvCXDgAa5xy0+3Sl0myxIs8rJ5KeXfJQFe7qxgaw " | ||
| 51 | -+ "VjJsJTKw8neOTw2rQfLaigWu2LIWw+" | ||
| 52 | -+ "IyVrLjZJdLqGkiLBGd1w4X3U12 " | ||
| 53 | -+ "fFxoY3eqzNgBEtduoGKPZ/" | ||
| 54 | -+ "NpP9cuKJORJ18283aV8hR4WO91VR0q1zcM jLwqUg=="), | ||
| 55 | -+ /* | ||
| 56 | -+ * Sentinel. | ||
| 57 | -+ */ | ||
| 58 | -+ TEXT_SENTINEL() | ||
| 59 | -+ }; | ||
| 60 | -+ wire_ok_t wire_ok[] = { | ||
| 61 | -+ WIRE_VALID(0x00, 0x06, 0x08, 0x00, 0x00, 0x01, 0x51, 0x80, 0x69, | ||
| 62 | -+ 0xee, 0x44, 0x90, 0x69, 0xdd, 0x13, 0x00, 0xd4, 0x79, | ||
| 63 | -+ 0x00, 0xb4, 0x56, 0xdc, 0xa1, 0x53, 0xfc, 0x32, 0x7a, | ||
| 64 | -+ 0x5e, 0x71, 0x4a, 0xae, 0x27, 0xf6, 0xa3, 0xf9, 0x77, | ||
| 65 | -+ 0x8d, 0x81, 0x5e, 0xed, 0xb3, 0xd1, 0x92, 0x1d, 0x59, | ||
| 66 | -+ 0x17, 0x69, 0x74, 0x49, 0xac, 0x9f, 0xd3, 0x12, 0x43, | ||
| 67 | -+ 0x99, 0x02, 0x95, 0x46, 0xd0, 0x5e, 0x84, 0xf6, 0xc6, | ||
| 68 | -+ 0x29, 0xc4, 0x69, 0x36, 0x22, 0xdc, 0x1e, 0x2c, 0xbe, | ||
| 69 | -+ 0x37, 0xba, 0x38, 0xa8, 0x1a, 0x3d, 0x07, 0x5b, 0x06, | ||
| 70 | -+ 0x43, 0x45, 0x1f, 0xaf, 0x6e, 0xea, 0xfe, 0x3b, 0xf7, | ||
| 71 | -+ 0x48, 0x76, 0x17, 0xa2, 0x8a, 0x0d, 0x55, 0xbd, 0x4f, | ||
| 72 | -+ 0xbd, 0x1d, 0x72, 0xe2, 0x40, 0xe5, 0x62, 0x63, 0xd2, | ||
| 73 | -+ 0x9c, 0xeb, 0x88, 0xb1, 0x77, 0x22, 0x61, 0x60, 0x20, | ||
| 74 | -+ 0xb9, 0x2b, 0xe6, 0xce, 0xd6, 0xab, 0x07, 0xc5, 0xdf, | ||
| 75 | -+ 0x8a, 0x25, 0x73, 0x9a, 0xc7, 0x21, 0xdb, 0xa9, 0xc8, | ||
| 76 | -+ 0x58, 0x90, 0x31, 0x72, 0xe0, 0xf2, 0xfa, 0xa9, 0xfb, | ||
| 77 | -+ 0x44, 0x55, 0xa2, 0xe4, 0xb9, 0xe4, 0x60, 0x8d, 0x78, | ||
| 78 | -+ 0x0e, 0xb2, 0xaf, 0xcd, 0xc6, 0xf0, 0x97, 0x0e, 0x00, | ||
| 79 | -+ 0x1a, 0xe7, 0x1c, 0xb4, 0xfb, 0x74, 0xa5, 0xd2, 0x6c, | ||
| 80 | -+ 0xb1, 0x22, 0xcf, 0x2b, 0x27, 0x92, 0x9e, 0x5d, 0xf2, | ||
| 81 | -+ 0x50, 0x15, 0xee, 0xea, 0xc6, 0x06, 0xb0, 0x56, 0x32, | ||
| 82 | -+ 0x6c, 0x25, 0x32, 0xb0, 0xf2, 0x77, 0x8e, 0x4f, 0x0d, | ||
| 83 | -+ 0xab, 0x41, 0xf2, 0xda, 0x8a, 0x05, 0xae, 0xd8, 0xb2, | ||
| 84 | -+ 0x16, 0xc3, 0xe2, 0x32, 0x56, 0xb2, 0xe3, 0x64, 0x97, | ||
| 85 | -+ 0x4b, 0xa8, 0x69, 0x22, 0x2c, 0x11, 0x9d, 0xd7, 0x0e, | ||
| 86 | -+ 0x17, 0xdd, 0x4d, 0x76, 0x7c, 0x5c, 0x68, 0x63, 0x77, | ||
| 87 | -+ 0xaa, 0xcc, 0xd8, 0x01, 0x12, 0xd7, 0x6e, 0xa0, 0x62, | ||
| 88 | -+ 0x8f, 0x67, 0xf3, 0x69, 0x3f, 0xd7, 0x2e, 0x28, 0x93, | ||
| 89 | -+ 0x91, 0x27, 0x5f, 0x36, 0xf3, 0x76, 0x95, 0xf2, 0x14, | ||
| 90 | -+ 0x78, 0x58, 0xef, 0x75, 0x55, 0x1d, 0x2a, 0xd7, 0x37, | ||
| 91 | -+ 0x0c, 0x8c, 0xbc, 0x2a, 0x52), | ||
| 92 | -+ /* labels too short for signer */ | ||
| 93 | -+ WIRE_INVALID( | ||
| 94 | -+ 0x00, 0x06, 0x08, 0x00, 0x00, 0x01, 0x51, 0x80, 0x69, | ||
| 95 | -+ 0xee, 0x44, 0x90, 0x69, 0xdd, 0x13, 0x00, 0xd4, 0x79, | ||
| 96 | -+ 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x00, | ||
| 97 | -+ 0xb4, 0x56, 0xdc, 0xa1, 0x53, 0xfc, 0x32, 0x7a, 0x5e, | ||
| 98 | -+ 0x71, 0x4a, 0xae, 0x27, 0xf6, 0xa3, 0xf9, 0x77, 0x8d, | ||
| 99 | -+ 0x81, 0x5e, 0xed, 0xb3, 0xd1, 0x92, 0x1d, 0x59, 0x17, | ||
| 100 | -+ 0x69, 0x74, 0x49, 0xac, 0x9f, 0xd3, 0x12, 0x43, 0x99, | ||
| 101 | -+ 0x02, 0x95, 0x46, 0xd0, 0x5e, 0x84, 0xf6, 0xc6, 0x29, | ||
| 102 | -+ 0xc4, 0x69, 0x36, 0x22, 0xdc, 0x1e, 0x2c, 0xbe, 0x37, | ||
| 103 | -+ 0xba, 0x38, 0xa8, 0x1a, 0x3d, 0x07, 0x5b, 0x06, 0x43, | ||
| 104 | -+ 0x45, 0x1f, 0xaf, 0x6e, 0xea, 0xfe, 0x3b, 0xf7, 0x48, | ||
| 105 | -+ 0x76, 0x17, 0xa2, 0x8a, 0x0d, 0x55, 0xbd, 0x4f, 0xbd, | ||
| 106 | -+ 0x1d, 0x72, 0xe2, 0x40, 0xe5, 0x62, 0x63, 0xd2, 0x9c, | ||
| 107 | -+ 0xeb, 0x88, 0xb1, 0x77, 0x22, 0x61, 0x60, 0x20, 0xb9, | ||
| 108 | -+ 0x2b, 0xe6, 0xce, 0xd6, 0xab, 0x07, 0xc5, 0xdf, 0x8a, | ||
| 109 | -+ 0x25, 0x73, 0x9a, 0xc7, 0x21, 0xdb, 0xa9, 0xc8, 0x58, | ||
| 110 | -+ 0x90, 0x31, 0x72, 0xe0, 0xf2, 0xfa, 0xa9, 0xfb, 0x44, | ||
| 111 | -+ 0x55, 0xa2, 0xe4, 0xb9, 0xe4, 0x60, 0x8d, 0x78, 0x0e, | ||
| 112 | -+ 0xb2, 0xaf, 0xcd, 0xc6, 0xf0, 0x97, 0x0e, 0x00, 0x1a, | ||
| 113 | -+ 0xe7, 0x1c, 0xb4, 0xfb, 0x74, 0xa5, 0xd2, 0x6c, 0xb1, | ||
| 114 | -+ 0x22, 0xcf, 0x2b, 0x27, 0x92, 0x9e, 0x5d, 0xf2, 0x50, | ||
| 115 | -+ 0x15, 0xee, 0xea, 0xc6, 0x06, 0xb0, 0x56, 0x32, 0x6c, | ||
| 116 | -+ 0x25, 0x32, 0xb0, 0xf2, 0x77, 0x8e, 0x4f, 0x0d, 0xab, | ||
| 117 | -+ 0x41, 0xf2, 0xda, 0x8a, 0x05, 0xae, 0xd8, 0xb2, 0x16, | ||
| 118 | -+ 0xc3, 0xe2, 0x32, 0x56, 0xb2, 0xe3, 0x64, 0x97, 0x4b, | ||
| 119 | -+ 0xa8, 0x69, 0x22, 0x2c, 0x11, 0x9d, 0xd7, 0x0e, 0x17, | ||
| 120 | -+ 0xdd, 0x4d, 0x76, 0x7c, 0x5c, 0x68, 0x63, 0x77, 0xaa, | ||
| 121 | -+ 0xcc, 0xd8, 0x01, 0x12, 0xd7, 0x6e, 0xa0, 0x62, 0x8f, | ||
| 122 | -+ 0x67, 0xf3, 0x69, 0x3f, 0xd7, 0x2e, 0x28, 0x93, 0x91, | ||
| 123 | -+ 0x27, 0x5f, 0x36, 0xf3, 0x76, 0x95, 0xf2, 0x14, 0x78, | ||
| 124 | -+ 0x58, 0xef, 0x75, 0x55, 0x1d, 0x2a, 0xd7, 0x37, 0x0c, | ||
| 125 | -+ 0x8c, 0xbc, 0x2a, 0x52), | ||
| 126 | -+ | ||
| 127 | -+ WIRE_SENTINEL() | ||
| 128 | -+ }; | ||
| 129 | -+ check_rdata(text_ok, wire_ok, NULL, false, dns_rdataclass_in, | ||
| 130 | -+ dns_rdatatype_rrsig, sizeof(dns_rdata_rrsig_t)); | ||
| 131 | -+} | ||
| 132 | -+ | ||
| 133 | - ISC_RUN_TEST_IMPL(resinfo) { | ||
| 134 | - text_ok_t text_ok[] = { | ||
| 135 | - TEXT_VALID_CHANGED("qnamemin exterr=15,16,17 " | ||
| 136 | -@@ -3532,6 +3641,7 @@ ISC_TEST_ENTRY(nsec3) | ||
| 137 | - ISC_TEST_ENTRY(nxt) | ||
| 138 | - ISC_TEST_ENTRY(resinfo) | ||
| 139 | - ISC_TEST_ENTRY(rkey) | ||
| 140 | -+ISC_TEST_ENTRY(rrsig) | ||
| 141 | - ISC_TEST_ENTRY(sshfp) | ||
| 142 | - ISC_TEST_ENTRY(wallet) | ||
| 143 | - ISC_TEST_ENTRY(wks) | ||
| 144 | --- | ||
| 145 | -2.43.0 | ||
| 146 | - | ||
| @@ -1,283 +0,0 @@ | |||
| 1 | -rom 773d46d58c693047a5945c8fe40512edd0ac214e Mon Sep 17 00:00:00 2001 | ||
| 2 | -From: Colin Vidal <colin@isc.org> | ||
| 3 | -Date: Thu, 18 Jun 2026 18:17:05 +0200 | ||
| 4 | -Subject: [PATCH] Do not assert in some CNAME/DNAME queries | ||
| 5 | - | ||
| 6 | -Fix a `named` crash because of a fail assertion for certains types of | ||
| 7 | -CNAME and DNAME queries: | ||
| 8 | - | ||
| 9 | -- If a client queries for a DNAME and A record to the resolver, and the | ||
| 10 | - authoritative server responds positively to the A query but delay the | ||
| 11 | - DNAME response and respond later negatively; | ||
| 12 | - | ||
| 13 | -- If a client queries for a CNAME and A record to the resolver, and the | ||
| 14 | - authoritative server responds positively to the A query but delay the | ||
| 15 | - CNAME response and respond later with a self-referential CNAME. | ||
| 16 | - | ||
| 17 | -The first scenario consists of sending two queries: `foo.test./DNAME` | ||
| 18 | -and `a.foo.test./A`. The authoritative server delays the answer for | ||
| 19 | -`foo.test./DNAME` but immediately answers the DNAME record for the | ||
| 20 | -second query: `foo.test. DNAME bar.test.`. The resolver caches it, | ||
| 21 | -follows the DNAME, and resolves `a.bar.test./A`. The authoritative | ||
| 22 | -server eventually answers negatively for `foo.test./DNAME` | ||
| 23 | -(NOERROR/NODATA, with only an SOA in the authority section). The | ||
| 24 | -resolver pulls out the previously cached rdataset (because it has a | ||
| 25 | -higher trust level than the received negative answer), and wrongly (this | ||
| 26 | -is the first bug) sets the result to `DNS_R_DNAME` instead of | ||
| 27 | -`ISC_R_SUCCESS`. The code in `ns/query.c` that handles the resolver | ||
| 28 | -result interprets this as "this is a non-DNAME query and we got a DNAME | ||
| 29 | -rdataset, so follow the chain". It goes into the `query_dname()` | ||
| 30 | -function, which asserts that the qname is a subdomain of the owner name | ||
| 31 | -in the rdataset. That assertion fails because the qname (`foo.test.`) is | ||
| 32 | -exactly equal to the owner name of the DNAME (`foo.test.`), rather than | ||
| 33 | -being a subdomain of it. `DNS_R_DNAME` must only be set when the qtype | ||
| 34 | -is something other than DNAME and the resolver has obtained a DNAME that | ||
| 35 | -needs to be followed. | ||
| 36 | - | ||
| 37 | -The second scenario consists of sending two queries: | ||
| 38 | -`cname.foo.test./CNAME` and `cname.foo.test./A`. The authoritative | ||
| 39 | -server delays the answer for `cname.foo.test./CNAME` but immediately | ||
| 40 | -answers the CNAME record for the second query: `cname.foo.test. CNAME | ||
| 41 | -cname.foo.test.`. Note that the CNAME is self-referential. The resolver | ||
| 42 | -caches it and sets the result code to `DNS_R_CNAME`. Then `ns/query.c` | ||
| 43 | -interprets this as "this is a non-CNAME query and we got a CNAME | ||
| 44 | -rdataset, so follow the chain" (which is correct in this case; however, | ||
| 45 | -because the CNAME rdataset is self-referential, the resolver responds | ||
| 46 | -with SERVFAIL, which is expected). The authoritative server eventually | ||
| 47 | -answers negatively for `cname.foo.test./CNAME`. The resolver then pulls | ||
| 48 | -out the previously cached CNAME rdataset (obtained from the A answer, | ||
| 49 | -even though it was self-referential, the resolver cached it) and wrongly | ||
| 50 | -sets the result to `DNS_R_CNAME` (this is the second bug). As noted | ||
| 51 | -above, `ns/query.c` interprets this as "this is a non-CNAME query and we | ||
| 52 | -got a CNAME rdataset, so follow the chain". The internals here are | ||
| 53 | -slightly more subtle: it first goes into `query_cname()` and sets the | ||
| 54 | -CNAME rdataset in the message answer section, then restarts the query to | ||
| 55 | -follow the CNAME. The restart retrieves the CNAME rdataset from the | ||
| 56 | -cache directly (without going to the resolver), and this time the query | ||
| 57 | -context result is `ISC_R_SUCCESS` (since it was found) and | ||
| 58 | -`qctx->rdataset` points to the same CNAME again (as it is | ||
| 59 | -self-referential), so it goes directly into the | ||
| 60 | -`query_prepresponse()/query_respond()` flow, which attempts to add the | ||
| 61 | -rdataset to the message answer again. However, this fails because the | ||
| 62 | -rdataset is already in the message, and the assertion which expects that | ||
| 63 | -operation to succeed fails (due to `qctx->rdataset` being set to `NULL` | ||
| 64 | -when ownership of the rdataset was transferred). `DNS_R_CNAME` must only | ||
| 65 | -be set when the qtype is something other than CNAME and the resolver has | ||
| 66 | -obtained a CNAME that needs to be followed. | ||
| 67 | - | ||
| 68 | -In both cases, the correct answer from the resolver should have been | ||
| 69 | -`ISC_R_SUCCESS` (instead of respectively `DNS_R_DNAME` and | ||
| 70 | -`DNS_R_CNAME`) becuase the rdataset that has been looked up was found. | ||
| 71 | - | ||
| 72 | -Conflict:context adapte | ||
| 73 | -Reference:https://gitlab.isc.org/isc-projects/bind9/-/commit/773d46d58c693047a5945c8fe40512edd0ac214e | ||
| 74 | ---- | ||
| 75 | - lib/dns/resolver.c | 138 ++++++++++++++++++++------------------------- | ||
| 76 | - 1 file changed, 61 insertions(+), 77 deletions(-) | ||
| 77 | - | ||
| 78 | -diff --git a/lib/dns/resolver.c b/lib/dns/resolver.c | ||
| 79 | -index a4be00d..32b4b6b 100644 | ||
| 80 | ---- a/lib/dns/resolver.c | ||
| 81 | -+++ b/lib/dns/resolver.c | ||
| 82 | -@@ -677,10 +677,10 @@ fctx_minimize_qname(fetchctx_t *fctx); | ||
| 83 | - static void | ||
| 84 | - fctx_destroy(fetchctx_t *fctx); | ||
| 85 | - static isc_result_t | ||
| 86 | --ncache_adderesult(dns_message_t *message, dns_db_t *cache, dns_dbnode_t *node, | ||
| 87 | -- dns_rdatatype_t covers, isc_stdtime_t now, dns_ttl_t minttl, | ||
| 88 | -- dns_ttl_t maxttl, bool optout, bool secure, | ||
| 89 | -- dns_rdataset_t *ardataset, isc_result_t *eresultp); | ||
| 90 | -+ncache_adderesult(fetchctx_t *fctx, dns_message_t *message, dns_dbnode_t *node, | ||
| 91 | -+ dns_rdatatype_t covers, isc_stdtime_t now, dns_ttl_t maxttl, | ||
| 92 | -+ bool optout, bool secure, dns_rdataset_t *ardataset, | ||
| 93 | -+ isc_result_t *eresultp); | ||
| 94 | - static void | ||
| 95 | - validated(void *arg); | ||
| 96 | - static void | ||
| 97 | -@@ -5282,6 +5282,46 @@ get_and_check_signer_name(dns_name_t *signer, dns_rdataset_t *sigrdataset) { | ||
| 98 | - return true; | ||
| 99 | - } | ||
| 100 | - | ||
| 101 | -+/* | ||
| 102 | -+ * After a (non-error) negative-cache add, 'rdataset' is bound to whatever | ||
| 103 | -+ * rdataset the cache authoritatively holds for the queried name and type. | ||
| 104 | -+ * Map that to the result code the fetch should report: | ||
| 105 | -+ * | ||
| 106 | -+ * - A negative cache entry (the one we just added, or a pre-existing one): | ||
| 107 | -+ * DNS_R_NCACHENXDOMAIN or DNS_R_NCACHENXRRSET, depending on NXDOMAIN vs | ||
| 108 | -+ * NODATA. | ||
| 109 | -+ * | ||
| 110 | -+ * - A positive rdataset that was already cached at higher trust, which | ||
| 111 | -+ * caused our negative entry to be discarded (e.g. a CNAME or DNAME cached | ||
| 112 | -+ * by a concurrent query): ISC_R_SUCCESS, because that cached positive | ||
| 113 | -+ * answer is what gets returned. Note the specific case for CNAME and | ||
| 114 | -+ * DNAME *if* the query type is not the same as the rdataset type. There | ||
| 115 | -+ * is a chain to follow *only* if the query type doesn't ask for the CNAME | ||
| 116 | -+ * or the DNAME. | ||
| 117 | -+ */ | ||
| 118 | -+static isc_result_t | ||
| 119 | -+fctx_setresult(fetchctx_t *fctx, dns_rdataset_t *rdataset) { | ||
| 120 | -+ isc_result_t result = ISC_R_SUCCESS; | ||
| 121 | -+ | ||
| 122 | -+ if (NEGATIVE(rdataset)) { | ||
| 123 | -+ result = NXDOMAIN(rdataset) ? DNS_R_NCACHENXDOMAIN | ||
| 124 | -+ : DNS_R_NCACHENXRRSET; | ||
| 125 | -+ } else if (result == ISC_R_SUCCESS && rdataset->type != fctx->type) { | ||
| 126 | -+ switch (rdataset->type) { | ||
| 127 | -+ case dns_rdatatype_cname: | ||
| 128 | -+ result = DNS_R_CNAME; | ||
| 129 | -+ break; | ||
| 130 | -+ case dns_rdatatype_dname: | ||
| 131 | -+ result = DNS_R_DNAME; | ||
| 132 | -+ break; | ||
| 133 | -+ default: | ||
| 134 | -+ break; | ||
| 135 | -+ } | ||
| 136 | -+ } | ||
| 137 | -+ | ||
| 138 | -+ return result; | ||
| 139 | -+} | ||
| 140 | -+ | ||
| 141 | - /* | ||
| 142 | - * The validator has finished. | ||
| 143 | - */ | ||
| 144 | -@@ -5529,8 +5569,7 @@ validated(void *arg) { | ||
| 145 | - ttl = 0; | ||
| 146 | - } | ||
| 147 | - | ||
| 148 | -- result = ncache_adderesult(message, fctx->cache, node, covers, | ||
| 149 | -- now, fctx->res->view->minncachettl, | ||
| 150 | -+ result = ncache_adderesult(fctx, message, node, covers, now, | ||
| 151 | - ttl, val->optout, val->secure, | ||
| 152 | - ardataset, &eresult); | ||
| 153 | - if (result != ISC_R_SUCCESS) { | ||
| 154 | -@@ -5786,23 +5825,7 @@ answer_response: | ||
| 155 | - */ | ||
| 156 | - INSIST(hresp->rdataset != NULL); | ||
| 157 | - if (dns_rdataset_isassociated(hresp->rdataset)) { | ||
| 158 | -- if (NEGATIVE(hresp->rdataset)) { | ||
| 159 | -- INSIST(eresult == DNS_R_NCACHENXDOMAIN || | ||
| 160 | -- eresult == DNS_R_NCACHENXRRSET); | ||
| 161 | -- } else if (eresult == ISC_R_SUCCESS && | ||
| 162 | -- hresp->rdataset->type != fctx->type) | ||
| 163 | -- { | ||
| 164 | -- switch (hresp->rdataset->type) { | ||
| 165 | -- case dns_rdatatype_cname: | ||
| 166 | -- eresult = DNS_R_CNAME; | ||
| 167 | -- break; | ||
| 168 | -- case dns_rdatatype_dname: | ||
| 169 | -- eresult = DNS_R_DNAME; | ||
| 170 | -- break; | ||
| 171 | -- default: | ||
| 172 | -- break; | ||
| 173 | -- } | ||
| 174 | -- } | ||
| 175 | -+ eresult = fctx_setresult(fctx, hresp->rdataset); | ||
| 176 | - } | ||
| 177 | - | ||
| 178 | - hresp->result = eresult; | ||
| 179 | -@@ -6454,24 +6477,7 @@ cache_name(fetchctx_t *fctx, dns_name_t *name, dns_message_t *message, | ||
| 180 | - * resp->result. | ||
| 181 | - */ | ||
| 182 | - if (dns_rdataset_isassociated(resp->rdataset)) { | ||
| 183 | -- if (NEGATIVE(resp->rdataset)) { | ||
| 184 | -- INSIST(eresult == | ||
| 185 | -- DNS_R_NCACHENXDOMAIN || | ||
| 186 | -- eresult == DNS_R_NCACHENXRRSET); | ||
| 187 | -- } else if (eresult == ISC_R_SUCCESS && | ||
| 188 | -- resp->rdataset->type != fctx->type) | ||
| 189 | -- { | ||
| 190 | -- switch (resp->rdataset->type) { | ||
| 191 | -- case dns_rdatatype_cname: | ||
| 192 | -- eresult = DNS_R_CNAME; | ||
| 193 | -- break; | ||
| 194 | -- case dns_rdatatype_dname: | ||
| 195 | -- eresult = DNS_R_DNAME; | ||
| 196 | -- break; | ||
| 197 | -- default: | ||
| 198 | -- break; | ||
| 199 | -- } | ||
| 200 | -- } | ||
| 201 | -+ eresult = fctx_setresult(fctx, resp->rdataset); | ||
| 202 | - } | ||
| 203 | - resp->result = eresult; | ||
| 204 | - if (adbp != NULL && *adbp != NULL) { | ||
| 205 | -@@ -6540,12 +6546,14 @@ cache_message(fetchctx_t *fctx, dns_message_t *message, | ||
| 206 | - * eresult. | ||
| 207 | - */ | ||
| 208 | - static isc_result_t | ||
| 209 | --ncache_adderesult(dns_message_t *message, dns_db_t *cache, dns_dbnode_t *node, | ||
| 210 | -- dns_rdatatype_t covers, isc_stdtime_t now, dns_ttl_t minttl, | ||
| 211 | -- dns_ttl_t maxttl, bool optout, bool secure, | ||
| 212 | -- dns_rdataset_t *ardataset, isc_result_t *eresultp) { | ||
| 213 | -+ncache_adderesult(fetchctx_t *fctx, dns_message_t *message, dns_dbnode_t *node, | ||
| 214 | -+ dns_rdatatype_t covers, isc_stdtime_t now, dns_ttl_t maxttl, | ||
| 215 | -+ bool optout, bool secure, dns_rdataset_t *ardataset, | ||
| 216 | -+ isc_result_t *eresultp) { | ||
| 217 | - isc_result_t result; | ||
| 218 | - dns_rdataset_t rdataset; | ||
| 219 | -+ dns_db_t *cache = fctx->cache; | ||
| 220 | -+ dns_ttl_t minttl = fctx->res->view->minncachettl; | ||
| 221 | - | ||
| 222 | - if (ardataset == NULL) { | ||
| 223 | - dns_rdataset_init(&rdataset); | ||
| 224 | -@@ -6559,39 +6567,16 @@ ncache_adderesult(dns_message_t *message, dns_db_t *cache, dns_dbnode_t *node, | ||
| 225 | - result = dns_ncache_add(message, cache, node, covers, now, | ||
| 226 | - minttl, maxttl, ardataset); | ||
| 227 | - } | ||
| 228 | -+ | ||
| 229 | - if (result == DNS_R_UNCHANGED || result == ISC_R_SUCCESS) { | ||
| 230 | - /* | ||
| 231 | -- * If the cache now contains a negative entry and we | ||
| 232 | -- * care about whether it is DNS_R_NCACHENXDOMAIN or | ||
| 233 | -- * DNS_R_NCACHENXRRSET then extract it. | ||
| 234 | -+ * The cache settled successfully (DNS_R_UNCHANGED means our | ||
| 235 | -+ * negative entry was discarded in favour of existing | ||
| 236 | -+ * higher-trust data). Either way 'ardataset' is now bound to | ||
| 237 | -+ * the rdataset the cache holds for this name and type; derive | ||
| 238 | -+ * the result code from it. | ||
| 239 | - */ | ||
| 240 | -- if (NEGATIVE(ardataset)) { | ||
| 241 | -- /* | ||
| 242 | -- * The cache data is a negative cache entry. | ||
| 243 | -- */ | ||
| 244 | -- if (NXDOMAIN(ardataset)) { | ||
| 245 | -- *eresultp = DNS_R_NCACHENXDOMAIN; | ||
| 246 | -- } else { | ||
| 247 | -- *eresultp = DNS_R_NCACHENXRRSET; | ||
| 248 | -- } | ||
| 249 | -- } else { | ||
| 250 | -- /* | ||
| 251 | -- * The attempt to add a negative cache entry | ||
| 252 | -- * was rejected. Set *eresultp to reflect | ||
| 253 | -- * the type of the dataset being returned. | ||
| 254 | -- */ | ||
| 255 | -- switch (ardataset->type) { | ||
| 256 | -- case dns_rdatatype_cname: | ||
| 257 | -- *eresultp = DNS_R_CNAME; | ||
| 258 | -- break; | ||
| 259 | -- case dns_rdatatype_dname: | ||
| 260 | -- *eresultp = DNS_R_DNAME; | ||
| 261 | -- break; | ||
| 262 | -- default: | ||
| 263 | -- *eresultp = ISC_R_SUCCESS; | ||
| 264 | -- break; | ||
| 265 | -- } | ||
| 266 | -- } | ||
| 267 | -+ *eresultp = fctx_setresult(fctx, ardataset); | ||
| 268 | - result = ISC_R_SUCCESS; | ||
| 269 | - } | ||
| 270 | - if (ardataset == &rdataset && dns_rdataset_isassociated(ardataset)) { | ||
| 271 | -@@ -6735,8 +6720,7 @@ ncache_message(fetchctx_t *fctx, dns_message_t *message, | ||
| 272 | - ttl = 0; | ||
| 273 | - } | ||
| 274 | - | ||
| 275 | -- result = ncache_adderesult(message, fctx->cache, node, covers, now, | ||
| 276 | -- fctx->res->view->minncachettl, ttl, false, | ||
| 277 | -+ result = ncache_adderesult(fctx, message, node, covers, now, ttl, false, | ||
| 278 | - false, ardataset, &eresult); | ||
| 279 | - if (result != ISC_R_SUCCESS) { | ||
| 280 | - goto unlock; | ||
| 281 | --- | ||
| 282 | -2.43.0 | ||
| 283 | - | ||
| @@ -1,288 +0,0 @@ | |||
| 1 | -From e88271f2e584010157b068cc998dd76451273562 Mon Sep 17 00:00:00 2001 | ||
| 2 | -From: Colin Vidal <colin@isc.org> | ||
| 3 | -Date: Mon, 15 Jun 2026 11:34:08 +0200 | ||
| 4 | -Subject: [PATCH] Reproducer for #5946 (assertion in some CNAME/DNAME queries) | ||
| 5 | - | ||
| 6 | -Add a system test reproducing the issue reported by #5946, which | ||
| 7 | -is also CVE-2026-12617. There are two scenarios: | ||
| 8 | - | ||
| 9 | -- A client send queries for a DNAME and A record to the resolver (ns3), | ||
| 10 | - and the authoritative server (ans2) responds positively to the A query | ||
| 11 | - but delay the DNAME response and respond later negatively; | ||
| 12 | - | ||
| 13 | -- A client send queries for a CNAME and A record to the resolver (ns3), | ||
| 14 | - and the authoritative server (ans2) responds positively to the A query | ||
| 15 | - but delay the CNAME response and respond later with a self-referential | ||
| 16 | - CNAME. | ||
| 17 | - | ||
| 18 | -The test does not check the results of the queries, however, it expects | ||
| 19 | -the resolver to correctly handle those and do not assert. | ||
| 20 | - | ||
| 21 | -Conflict:no | ||
| 22 | -Reference:https://gitlab.isc.org/isc-projects/bind9/-/commit/e88271f2e584010157b068cc998dd76451273562 | ||
| 23 | ---- | ||
| 24 | - .../system/cname_dname_negcache/ans2/ans.py | 98 +++++++++++++++++++ | ||
| 25 | - .../cname_dname_negcache/ns1/bar.test.db | 5 + | ||
| 26 | - .../cname_dname_negcache/ns1/named.conf.j2 | 24 +++++ | ||
| 27 | - .../system/cname_dname_negcache/ns1/root.db | 6 ++ | ||
| 28 | - .../system/cname_dname_negcache/ns1/test.db | 8 ++ | ||
| 29 | - .../cname_dname_negcache/ns3/named.conf.j2 | 11 +++ | ||
| 30 | - .../tests_cname_dname_negcache.py | 52 ++++++++++ | ||
| 31 | - 7 files changed, 204 insertions(+) | ||
| 32 | - create mode 100644 bin/tests/system/cname_dname_negcache/ans2/ans.py | ||
| 33 | - create mode 100644 bin/tests/system/cname_dname_negcache/ns1/bar.test.db | ||
| 34 | - create mode 100644 bin/tests/system/cname_dname_negcache/ns1/named.conf.j2 | ||
| 35 | - create mode 100644 bin/tests/system/cname_dname_negcache/ns1/root.db | ||
| 36 | - create mode 100644 bin/tests/system/cname_dname_negcache/ns1/test.db | ||
| 37 | - create mode 100644 bin/tests/system/cname_dname_negcache/ns3/named.conf.j2 | ||
| 38 | - create mode 100644 bin/tests/system/cname_dname_negcache/tests_cname_dname_negcache.py | ||
| 39 | - | ||
| 40 | -diff --git a/bin/tests/system/cname_dname_negcache/ans2/ans.py b/bin/tests/system/cname_dname_negcache/ans2/ans.py | ||
| 41 | -new file mode 100644 | ||
| 42 | -index 0000000000..eec5c90297 | ||
| 43 | ---- /dev/null | ||
| 44 | -+++ b/bin/tests/system/cname_dname_negcache/ans2/ans.py | ||
| 45 | -@@ -0,0 +1,98 @@ | ||
| 46 | -+""" | ||
| 47 | -+Copyright (C) Internet Systems Consortium, Inc. ("ISC") | ||
| 48 | -+ | ||
| 49 | -+SPDX-License-Identifier: MPL-2.0 | ||
| 50 | -+ | ||
| 51 | -+This Source Code Form is subject to the terms of the Mozilla Public | ||
| 52 | -+License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| 53 | -+file, you can obtain one at https://mozilla.org/MPL/2.0/. | ||
| 54 | -+ | ||
| 55 | -+See the COPYRIGHT file distributed with this work for additional | ||
| 56 | -+information regarding copyright ownership. | ||
| 57 | -+""" | ||
| 58 | -+ | ||
| 59 | -+from collections.abc import AsyncGenerator | ||
| 60 | -+ | ||
| 61 | -+from dns import name, rcode, rdataclass, rdatatype, rrset | ||
| 62 | -+ | ||
| 63 | -+from isctest.asyncserver import ( | ||
| 64 | -+ AsyncDnsServer, | ||
| 65 | -+ DnsResponseSend, | ||
| 66 | -+ QnameQtypeHandler, | ||
| 67 | -+ QueryContext, | ||
| 68 | -+ StaticResponseHandler, | ||
| 69 | -+) | ||
| 70 | -+ | ||
| 71 | -+ | ||
| 72 | -+def build_rrset( | ||
| 73 | -+ qname: name.Name | str, | ||
| 74 | -+ rtype: rdatatype.RdataType, | ||
| 75 | -+ rdata: str, | ||
| 76 | -+ ttl: int = 300, | ||
| 77 | -+) -> rrset.RRset: | ||
| 78 | -+ return rrset.from_text(qname, ttl, rdataclass.IN, rtype, rdata) | ||
| 79 | -+ | ||
| 80 | -+ | ||
| 81 | -+class FooTestNsHandler(QnameQtypeHandler, StaticResponseHandler): | ||
| 82 | -+ qnames = ["foo.test."] | ||
| 83 | -+ qtypes = [rdatatype.NS] | ||
| 84 | -+ answer = [build_rrset("foo.test.", rdatatype.NS, "ns.foo.test.")] | ||
| 85 | -+ additional = [build_rrset("ns.foo.test.", rdatatype.A, "10.53.0.2")] | ||
| 86 | -+ | ||
| 87 | -+ | ||
| 88 | -+class DelayedDnameNegHandler(QnameQtypeHandler, StaticResponseHandler): | ||
| 89 | -+ qnames = ["foo.test."] | ||
| 90 | -+ qtypes = [rdatatype.DNAME] | ||
| 91 | -+ authority = [ | ||
| 92 | -+ build_rrset( | ||
| 93 | -+ "foo.test.", | ||
| 94 | -+ rdatatype.SOA, | ||
| 95 | -+ "ns.test. op.ns.test. 2081509183 86400 3600 3600000 300", | ||
| 96 | -+ ) | ||
| 97 | -+ ] | ||
| 98 | -+ delay = 1 | ||
| 99 | -+ | ||
| 100 | -+ | ||
| 101 | -+class DnamePosHandler(QnameQtypeHandler, StaticResponseHandler): | ||
| 102 | -+ qnames = ["a.foo.test."] | ||
| 103 | -+ qtypes = [rdatatype.A] | ||
| 104 | -+ answer = [ | ||
| 105 | -+ build_rrset("foo.test.", rdatatype.DNAME, "bar.test."), | ||
| 106 | -+ build_rrset("a.foo.test.", rdatatype.CNAME, "a.bar.test."), | ||
| 107 | -+ ] | ||
| 108 | -+ | ||
| 109 | -+ | ||
| 110 | -+class CnameHandler(QnameQtypeHandler): | ||
| 111 | -+ qnames = ["cname.foo.test."] | ||
| 112 | -+ qtypes = [rdatatype.CNAME, rdatatype.A] | ||
| 113 | -+ answer = [build_rrset("cname.foo.test.", rdatatype.CNAME, "cname.foo.test.")] | ||
| 114 | -+ authority = [ | ||
| 115 | -+ build_rrset( | ||
| 116 | -+ "cname.foo.test.", | ||
| 117 | -+ rdatatype.SOA, | ||
| 118 | -+ "ns.test. op.ns.test. 2081509183 86400 3600 3600000 300", | ||
| 119 | -+ ) | ||
| 120 | -+ ] | ||
| 121 | -+ | ||
| 122 | -+ async def get_responses( | ||
| 123 | -+ self, qctx: QueryContext | ||
| 124 | -+ ) -> AsyncGenerator[DnsResponseSend, None]: | ||
| 125 | -+ qctx.prepare_new_response(with_zone_data=False) | ||
| 126 | -+ if qctx.qtype == rdatatype.CNAME: | ||
| 127 | -+ qctx.response.authority.extend(self.authority) | ||
| 128 | -+ yield DnsResponseSend(qctx.response, authoritative=True, delay=1) | ||
| 129 | -+ else: | ||
| 130 | -+ qctx.response.answer.extend(self.answer) | ||
| 131 | -+ yield DnsResponseSend(qctx.response, authoritative=True) | ||
| 132 | -+ | ||
| 133 | -+ | ||
| 134 | -+def main() -> None: | ||
| 135 | -+ server = AsyncDnsServer(default_aa=True, default_rcode=rcode.NOERROR) | ||
| 136 | -+ server.install_response_handlers( | ||
| 137 | -+ FooTestNsHandler(), DelayedDnameNegHandler(), DnamePosHandler(), CnameHandler() | ||
| 138 | -+ ) | ||
| 139 | -+ server.run() | ||
| 140 | -+ | ||
| 141 | -+ | ||
| 142 | -+if __name__ == "__main__": | ||
| 143 | -+ main() | ||
| 144 | -diff --git a/bin/tests/system/cname_dname_negcache/ns1/bar.test.db b/bin/tests/system/cname_dname_negcache/ns1/bar.test.db | ||
| 145 | -new file mode 100644 | ||
| 146 | -index 0000000000..840b9c3a2c | ||
| 147 | ---- /dev/null | ||
| 148 | -+++ b/bin/tests/system/cname_dname_negcache/ns1/bar.test.db | ||
| 149 | -@@ -0,0 +1,5 @@ | ||
| 150 | -+$TTL 300 | ||
| 151 | -+bar.test. IN SOA ns.bar.test. hostmaster.bar.test. 1 600 600 1200 600 | ||
| 152 | -+bar.test. NS ns.bar.test. | ||
| 153 | -+ns A 10.53.0.1 | ||
| 154 | -+a A 10.0.0.1 | ||
| 155 | -diff --git a/bin/tests/system/cname_dname_negcache/ns1/named.conf.j2 b/bin/tests/system/cname_dname_negcache/ns1/named.conf.j2 | ||
| 156 | -new file mode 100644 | ||
| 157 | -index 0000000000..d72dd1181d | ||
| 158 | ---- /dev/null | ||
| 159 | -+++ b/bin/tests/system/cname_dname_negcache/ns1/named.conf.j2 | ||
| 160 | -@@ -0,0 +1,24 @@ | ||
| 161 | -+options { | ||
| 162 | -+ query-source address @ns.ip@; | ||
| 163 | -+ port @PORT@; | ||
| 164 | -+ pid-file "named.pid"; | ||
| 165 | -+ listen-on { @ns.ip@; }; | ||
| 166 | -+ listen-on-v6 { none; }; | ||
| 167 | -+ recursion no; | ||
| 168 | -+ dnssec-validation no; | ||
| 169 | -+}; | ||
| 170 | -+ | ||
| 171 | -+zone "." { | ||
| 172 | -+ type primary; | ||
| 173 | -+ file "root.db"; | ||
| 174 | -+}; | ||
| 175 | -+ | ||
| 176 | -+zone "test." { | ||
| 177 | -+ type primary; | ||
| 178 | -+ file "test.db"; | ||
| 179 | -+}; | ||
| 180 | -+ | ||
| 181 | -+zone "bar.test." { | ||
| 182 | -+ type primary; | ||
| 183 | -+ file "bar.test.db"; | ||
| 184 | -+}; | ||
| 185 | -diff --git a/bin/tests/system/cname_dname_negcache/ns1/root.db b/bin/tests/system/cname_dname_negcache/ns1/root.db | ||
| 186 | -new file mode 100644 | ||
| 187 | -index 0000000000..c456c45b9d | ||
| 188 | ---- /dev/null | ||
| 189 | -+++ b/bin/tests/system/cname_dname_negcache/ns1/root.db | ||
| 190 | -@@ -0,0 +1,6 @@ | ||
| 191 | -+$TTL 300 | ||
| 192 | -+. IN SOA ns. hostmaster. 1 600 600 1200 600 | ||
| 193 | -+. NS a.root-servers.nil. | ||
| 194 | -+a.root-servers.nil. A 10.53.0.1 | ||
| 195 | -+test NS ns.test | ||
| 196 | -+ns.test A 10.53.0.1 | ||
| 197 | -diff --git a/bin/tests/system/cname_dname_negcache/ns1/test.db b/bin/tests/system/cname_dname_negcache/ns1/test.db | ||
| 198 | -new file mode 100644 | ||
| 199 | -index 0000000000..acb68e00f8 | ||
| 200 | ---- /dev/null | ||
| 201 | -+++ b/bin/tests/system/cname_dname_negcache/ns1/test.db | ||
| 202 | -@@ -0,0 +1,8 @@ | ||
| 203 | -+$TTL 300 | ||
| 204 | -+test. IN SOA ns.test. hostmaster.test. 1 600 600 1200 600 | ||
| 205 | -+test. NS ns.test. | ||
| 206 | -+ns A 10.53.0.1 | ||
| 207 | -+bar NS ns.bar | ||
| 208 | -+ns.bar A 10.53.0.1 | ||
| 209 | -+foo NS ns.foo | ||
| 210 | -+ns.foo A 10.53.0.2 | ||
| 211 | -diff --git a/bin/tests/system/cname_dname_negcache/ns3/named.conf.j2 b/bin/tests/system/cname_dname_negcache/ns3/named.conf.j2 | ||
| 212 | -new file mode 100644 | ||
| 213 | -index 0000000000..197d72756b | ||
| 214 | ---- /dev/null | ||
| 215 | -+++ b/bin/tests/system/cname_dname_negcache/ns3/named.conf.j2 | ||
| 216 | -@@ -0,0 +1,11 @@ | ||
| 217 | -+options { | ||
| 218 | -+ query-source address @ns.ip@; | ||
| 219 | -+ port @PORT@; | ||
| 220 | -+ pid-file "named.pid"; | ||
| 221 | -+ listen-on { @ns.ip@; }; | ||
| 222 | -+ listen-on-v6 { none; }; | ||
| 223 | -+ recursion yes; | ||
| 224 | -+ dnssec-validation no; | ||
| 225 | -+}; | ||
| 226 | -+ | ||
| 227 | -+{% include "_common/root.hint.conf" %} | ||
| 228 | -diff --git a/bin/tests/system/cname_dname_negcache/tests_cname_dname_negcache.py b/bin/tests/system/cname_dname_negcache/tests_cname_dname_negcache.py | ||
| 229 | -new file mode 100644 | ||
| 230 | -index 0000000000..65c44402fb | ||
| 231 | ---- /dev/null | ||
| 232 | -+++ b/bin/tests/system/cname_dname_negcache/tests_cname_dname_negcache.py | ||
| 233 | -@@ -0,0 +1,52 @@ | ||
| 234 | -+# Copyright (C) Internet Systems Consortium, Inc. ("ISC") | ||
| 235 | -+# | ||
| 236 | -+# SPDX-License-Identifier: MPL-2.0 | ||
| 237 | -+# | ||
| 238 | -+# This Source Code Form is subject to the terms of the Mozilla Public | ||
| 239 | -+# License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| 240 | -+# file, you can obtain one at https://mozilla.org/MPL/2.0/. | ||
| 241 | -+# | ||
| 242 | -+# See the COPYRIGHT file distributed with this work for additional | ||
| 243 | -+# information regarding copyright ownership. | ||
| 244 | -+ | ||
| 245 | -+from re import compile as Re | ||
| 246 | -+from socket import AF_INET, SOCK_DGRAM, socket | ||
| 247 | -+ | ||
| 248 | -+import isctest | ||
| 249 | -+ | ||
| 250 | -+ | ||
| 251 | -+def run_attack(ns, name1, type1, name2, type2): | ||
| 252 | -+ msg1 = isctest.query.create(name1, type1, cd=True) | ||
| 253 | -+ msg2 = isctest.query.create(name2, type2, cd=True) | ||
| 254 | -+ port = int(isctest.vars.ALL["PORT"]) | ||
| 255 | -+ | ||
| 256 | -+ with socket(AF_INET, SOCK_DGRAM) as sock: | ||
| 257 | -+ # The order the request does out doesn't matter. What is important is | ||
| 258 | -+ # the first query starts recursion before the second query returns the | ||
| 259 | -+ # answer, and the second query returns the answer before the first | ||
| 260 | -+ # query returns the answer. (So, when the NOERROR/NODATA cames back | ||
| 261 | -+ # from the first query, the cache is queried and we get the positive | ||
| 262 | -+ # response cached from the second query attached to the fresp rdataset | ||
| 263 | -+ # of the response of the first query.) | ||
| 264 | -+ # Therefore, the logic is really baked into ans2, which has a 3 seconds | ||
| 265 | -+ # delay to answer the first query. | ||
| 266 | -+ sock.sendto(msg1.to_wire(), (ns.ip, port)) | ||
| 267 | -+ sock.sendto(msg2.to_wire(), (ns.ip, port)) | ||
| 268 | -+ | ||
| 269 | -+ # The second query come back immediately, the resolver caches the DNAME. | ||
| 270 | -+ # The first query come back after 3s (because of intentional ans2 latency | ||
| 271 | -+ # on foo.test./DNAME answer) and should not crash the server. | ||
| 272 | -+ with ns.watch_log_from_start(timeout=15) as watcher: | ||
| 273 | -+ watcher.wait_for_sequence( | ||
| 274 | -+ [ | ||
| 275 | -+ Re(r"foo\.test\..*IN\s+SOA\s+ns\.test\.\s+op\.ns\.test\."), | ||
| 276 | -+ ] | ||
| 277 | -+ ) | ||
| 278 | -+ | ||
| 279 | -+ | ||
| 280 | -+def test_dname_negcache(ns3): | ||
| 281 | -+ run_attack(ns3, "foo.test.", "DNAME", "a.foo.test.", "A") | ||
| 282 | -+ | ||
| 283 | -+ | ||
| 284 | -+def test_cname_negcache(ns3): | ||
| 285 | -+ run_attack(ns3, "cname.foo.test.", "CNAME", "cname.foo.test.", "A") | ||
| 286 | --- | ||
| 287 | -2.43.0 | ||
| 288 | - | ||