diff --git a/bind-9.18-CVE-2026-10723-test.patch b/bind-9.18-CVE-2026-10723-test.patch new file mode 100644 index 0000000..04bff8a --- /dev/null +++ b/bind-9.18-CVE-2026-10723-test.patch @@ -0,0 +1,854 @@ +From 2a4786b0edde59274f682c9bd4ae4026c2d08218 Mon Sep 17 00:00:00 2001 +From: Evan Hunt +Date: Wed, 1 Jul 2026 22:56:02 -0700 +Subject: [PATCH] add isctest.mark method for ecdsa_deterinistic + +This checks support for ECDSA deterministic mode in the cryptography +library. + +(cherry picked from commit 6e44151466864d3dd783a20da83d01028781d3e2) +(cherry picked from commit dc3f52388427f4f94087d984d5a2088b925810e2) + +Reproducer for #5874 NSEC3 impersonation + +LLM generated. + +(cherry picked from commit f3e2eb333be3ac636f745aa13cfb8d9ee8af87d8) +(cherry picked from commit 35e3d49d2222c13786a06021c7ed583d2a656e51) + +Update reproducer #5874 + +Update the llm generated reproducer: +- Move server.py into ans1/ans.py +- Remove unnecessary named.conf configuration options +- Add comments describing the steps (copied from GL issue) +- Rename system test + +(cherry picked from commit c1321fef165a2ef8c2bff971901c58941e8e694c) +(cherry picked from commit 833dd3b230b92596074e8da15b12298f46c939f2) +--- + bin/tests/system/chain/ans3/ans.py | 10 +- + bin/tests/system/chain/ans4/ans.py | 8 +- + bin/tests/system/cookie/ans9/ans.py | 7 +- + bin/tests/system/digdelv/ans8/ans.py | 7 +- + bin/tests/system/dnssec/ans10/ans.py | 11 +- + bin/tests/system/forward/ans11/ans.py | 12 +- + bin/tests/system/isctest/mark.py | 12 + + .../system/nsec3_impersonation/ans1/ans.py | 280 ++++++++++++++++++ + .../nsec3_impersonation/ns2/named.conf.j2 | 33 +++ + .../tests_nsec3_impersonation.py | 152 ++++++++++ + bin/tests/system/qmin/ans2/ans.py | 11 +- + bin/tests/system/qmin/ans3/ans.py | 11 +- + bin/tests/system/qmin/ans4/ans.py | 11 +- + bin/tests/system/resolver/ans10/ans.py | 12 +- + 14 files changed, 529 insertions(+), 48 deletions(-) + create mode 100644 bin/tests/system/nsec3_impersonation/ans1/ans.py + create mode 100644 bin/tests/system/nsec3_impersonation/ns2/named.conf.j2 + create mode 100644 bin/tests/system/nsec3_impersonation/tests_nsec3_impersonation.py + +diff --git a/bin/tests/system/chain/ans3/ans.py b/bin/tests/system/chain/ans3/ans.py +index 0a031c1145..7c54c3c51a 100644 +--- a/bin/tests/system/chain/ans3/ans.py ++++ b/bin/tests/system/chain/ans3/ans.py +@@ -19,10 +19,10 @@ import sys + import signal + import socket + import select +-from datetime import datetime, timedelta +-import functools + +-import dns, dns.message, dns.query ++import dns ++import dns.message ++import dns.query + from dns.rdatatype import * + from dns.rdataclass import * + from dns.rcode import * +@@ -173,9 +173,9 @@ else: + while running: + try: + inputready, outputready, exceptready = select.select(input, [], []) +- except select.error as e: ++ except select.error: + break +- except socket.error as e: ++ except socket.error: + break + except KeyboardInterrupt: + break +diff --git a/bin/tests/system/chain/ans4/ans.py b/bin/tests/system/chain/ans4/ans.py +index c969117368..de536b25bd 100755 +--- a/bin/tests/system/chain/ans4/ans.py ++++ b/bin/tests/system/chain/ans4/ans.py +@@ -22,7 +22,9 @@ import select + from datetime import datetime, timedelta + import functools + +-import dns, dns.message, dns.query ++import dns ++import dns.message ++import dns.query + from dns.rdatatype import * + from dns.rdataclass import * + from dns.rcode import * +@@ -371,9 +373,9 @@ else: + while running: + try: + inputready, outputready, exceptready = select.select(input, [], []) +- except select.error as e: ++ except select.error: + break +- except socket.error as e: ++ except socket.error: + break + except KeyboardInterrupt: + break +diff --git a/bin/tests/system/cookie/ans9/ans.py b/bin/tests/system/cookie/ans9/ans.py +index 3b0f82cc1d..2710386d74 100644 +--- a/bin/tests/system/cookie/ans9/ans.py ++++ b/bin/tests/system/cookie/ans9/ans.py +@@ -15,9 +15,6 @@ import sys + import signal + import socket + import select +-from datetime import datetime, timedelta +-import time +-import functools + + import dns + import dns.edns +@@ -257,9 +254,9 @@ else: + while running: + try: + inputready, outputready, exceptready = select.select(input, [], []) +- except select.error as e: ++ except select.error: + break +- except socket.error as e: ++ except socket.error: + break + except KeyboardInterrupt: + break +diff --git a/bin/tests/system/digdelv/ans8/ans.py b/bin/tests/system/digdelv/ans8/ans.py +index 3e18edc1cc..1896a2bafc 100644 +--- a/bin/tests/system/digdelv/ans8/ans.py ++++ b/bin/tests/system/digdelv/ans8/ans.py +@@ -17,7 +17,8 @@ import socket + import select + import struct + +-import dns, dns.message ++import dns ++import dns.message + from dns.rcode import * + + modes = [ +@@ -109,9 +110,9 @@ hung_conns = [] + while running: + try: + inputready, outputready, exceptready = select.select(input, [], []) +- except select.error as e: ++ except select.error: + break +- except socket.error as e: ++ except socket.error: + break + except KeyboardInterrupt: + break +diff --git a/bin/tests/system/dnssec/ans10/ans.py b/bin/tests/system/dnssec/ans10/ans.py +index 84bf0a2642..46af72a147 100644 +--- a/bin/tests/system/dnssec/ans10/ans.py ++++ b/bin/tests/system/dnssec/ans10/ans.py +@@ -16,10 +16,11 @@ import signal + import socket + import select + from datetime import datetime, timedelta +-import time +-import functools + +-import dns, dns.message, dns.query, dns.flags ++import dns ++import dns.message ++import dns.query ++import dns.flags + from dns.rdatatype import * + from dns.rdataclass import * + from dns.rcode import * +@@ -140,9 +141,9 @@ else: + while running: + try: + inputready, outputready, exceptready = select.select(input, [], []) +- except select.error as e: ++ except select.error: + break +- except socket.error as e: ++ except socket.error: + break + except KeyboardInterrupt: + break +diff --git a/bin/tests/system/forward/ans11/ans.py b/bin/tests/system/forward/ans11/ans.py +index 00b5895f76..d5b8a5b037 100644 +--- a/bin/tests/system/forward/ans11/ans.py ++++ b/bin/tests/system/forward/ans11/ans.py +@@ -16,11 +16,11 @@ import signal + import socket + import select + import struct +-from datetime import datetime, timedelta +-import time +-import functools + +-import dns, dns.message, dns.query, dns.flags ++import dns ++import dns.message ++import dns.query ++import dns.flags + from dns.rdatatype import * + from dns.rdataclass import * + from dns.rcode import * +@@ -192,9 +192,9 @@ hung_conns = [] + while running: + try: + inputready, outputready, exceptready = select.select(input, [], []) +- except select.error as e: ++ except select.error: + break +- except socket.error as e: ++ except socket.error: + break + except KeyboardInterrupt: + break +diff --git a/bin/tests/system/isctest/mark.py b/bin/tests/system/isctest/mark.py +index 53860a806c..098af5acfc 100644 +--- a/bin/tests/system/isctest/mark.py ++++ b/bin/tests/system/isctest/mark.py +@@ -88,3 +88,15 @@ softhsm2_environment = pytest.mark.skipif( + ), + reason="SOFTHSM2_CONF and SOFTHSM2_MODULE environmental variables must be set and pkcs11-tool and softhsm2-util tools present", + ) ++ ++ecdsa_deterministic = False ++try: ++ from cryptography.hazmat.backends import default_backend ++ ++ ecdsa_deterministic = default_backend().ecdsa_deterministic_supported() ++except Exception: # pylint: disable=broad-except ++ pass ++ ++with_ecdsa_deterministic = pytest.mark.skipif( ++ not ecdsa_deterministic, reason="ECDSA deterministic signing is not supported" ++) +diff --git a/bin/tests/system/nsec3_impersonation/ans1/ans.py b/bin/tests/system/nsec3_impersonation/ans1/ans.py +new file mode 100644 +index 0000000000..177e79c195 +--- /dev/null ++++ b/bin/tests/system/nsec3_impersonation/ans1/ans.py +@@ -0,0 +1,280 @@ ++#!/usr/bin/python3 ++ ++# Copyright (C) Internet Systems Consortium, Inc. ("ISC") ++# ++# SPDX-License-Identifier: MPL-2.0 ++# ++# This Source Code Form is subject to the terms of the Mozilla Public ++# License, v. 2.0. If a copy of the MPL was not distributed with this ++# file, you can obtain one at https://mozilla.org/MPL/2.0/. ++# ++# See the COPYRIGHT file distributed with this work for additional ++# information regarding copyright ownership. ++ ++from collections.abc import AsyncGenerator ++from dataclasses import dataclass ++from pathlib import Path ++ ++import json ++ ++from cryptography.hazmat.primitives import serialization ++ ++import dns.dnssec ++import dns.flags ++import dns.message ++import dns.name ++import dns.rcode ++import dns.rdata ++import dns.rdataclass ++import dns.rdatatype ++import dns.rrset ++ ++from isctest.asyncserver import ( ++ AsyncDnsServer, ++ DnsResponseSend, ++ QueryContext, ++ ResponseHandler, ++) ++ ++TTL = 300 ++TLD = "tld.test." ++APEX_HASH = "1B40241KFORIOG780N4IKSCRLVETPCTQ" ++ATTACKER = f"{APEX_HASH.lower()}.{TLD}" ++VICTIM = f"victim.{TLD}" ++AUTH_IP = "10.53.0.1" ++ ++ ++@dataclass(frozen=True) ++class Key: ++ zone: dns.name.Name ++ private_key: object ++ dnskey: dns.rdata.Rdata ++ ds: dns.rdata.Rdata ++ ++ ++def name(text: str) -> dns.name.Name: ++ return dns.name.from_text(text) ++ ++ ++def load_keys() -> dict[str, Key]: ++ path = Path(__file__).resolve().parent / "keys.json" ++ with path.open(encoding="utf-8") as keys_file: ++ raw_keys = json.load(keys_file) ++ ++ keys = {} ++ for zone, raw_key in raw_keys.items(): ++ private_key = serialization.load_pem_private_key( ++ raw_key["private_pem"].encode("ascii"), ++ password=None, ++ ) ++ dnskey = dns.rdata.from_text( ++ dns.rdataclass.IN, dns.rdatatype.DNSKEY, raw_key["dnskey"] ++ ) ++ ds = dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.DS, raw_key["ds"]) ++ keys[zone] = Key(name(zone), private_key, dnskey, ds) ++ ++ return keys ++ ++ ++def rrset(owner: str, rdtype: dns.rdatatype.RdataType, *rdatas: str) -> dns.rrset.RRset: ++ return dns.rrset.from_text(owner, TTL, dns.rdataclass.IN, rdtype, *rdatas) ++ ++ ++def rrset_from_rdata(owner: str, rdata: dns.rdata.Rdata) -> dns.rrset.RRset: ++ return dns.rrset.from_rdata(name(owner), TTL, rdata) ++ ++ ++def rrsig_rrset(covered: dns.rrset.RRset, signer: Key) -> dns.rrset.RRset: ++ rrsig = dns.dnssec.sign( ++ covered, ++ signer.private_key, ++ signer.zone, ++ signer.dnskey, ++ lifetime=86400, ++ verify=True, ++ ) ++ return dns.rrset.from_rdata(covered.name, covered.ttl, rrsig) ++ ++ ++def add_signed( ++ section: list[dns.rrset.RRset], covered: dns.rrset.RRset, signer: Key ++) -> None: ++ section.append(covered) ++ section.append(rrsig_rrset(covered, signer)) ++ ++ ++def dnskey_rrset(zone: str, zone_key: Key) -> dns.rrset.RRset: ++ return rrset_from_rdata(zone, zone_key.dnskey) ++ ++ ++def ds_rrset(zone: str, zone_key: Key) -> dns.rrset.RRset: ++ return rrset_from_rdata(zone, zone_key.ds) ++ ++ ++def soa_rrset(zone: str) -> dns.rrset.RRset: ++ return rrset( ++ zone, ++ dns.rdatatype.SOA, ++ f"ns.{zone} hostmaster.{zone} 1 3600 600 86400 300", ++ ) ++ ++ ++def ns_rrset(zone: str, ns_target: str) -> dns.rrset.RRset: ++ return rrset(zone, dns.rdatatype.NS, ns_target) ++ ++ ++def glue_rrset(ns_target: str, address: str) -> dns.rrset.RRset: ++ return rrset(ns_target, dns.rdatatype.A, address) ++ ++ ++def answer_dnskey(response: dns.message.Message, zone: str, zone_key: Key) -> None: ++ add_signed(response.answer, dnskey_rrset(zone, zone_key), zone_key) ++ ++ ++def answer_soa(response: dns.message.Message, zone: str, zone_key: Key) -> None: ++ add_signed(response.answer, soa_rrset(zone), zone_key) ++ ++ ++def answer_ns( ++ response: dns.message.Message, zone: str, ns_target: str, zone_key: Key ++) -> None: ++ add_signed(response.answer, ns_rrset(zone, ns_target), zone_key) ++ ++ ++class SignedResponseHandler(ResponseHandler): ++ def __init__(self, keys: dict[str, Key]) -> None: ++ self.keys = keys ++ ++ async def get_responses( ++ self, qctx: QueryContext ++ ) -> AsyncGenerator[DnsResponseSend, None]: ++ qctx.prepare_new_response(with_zone_data=False) ++ qctx.response.flags |= dns.flags.AA ++ qctx.response.set_rcode(dns.rcode.NOERROR) ++ self.respond(qctx) ++ yield DnsResponseSend(qctx.response, authoritative=True) ++ ++ def respond(self, qctx: QueryContext) -> None: ++ raise NotImplementedError ++ ++ ++def child_nsec3_rrset() -> dns.rrset.RRset: ++ rdata = dns.rdata.from_text( ++ dns.rdataclass.IN, ++ dns.rdatatype.NSEC3, ++ f"1 0 0 - {APEX_HASH} NS SOA RRSIG DNSKEY NSEC3PARAM", ++ ) ++ return dns.rrset.from_rdata(name(f"{APEX_HASH}.{TLD}"), TTL, rdata) ++ ++ ++def forged_nxdomain(response: dns.message.Message, keys: dict[str, Key]) -> None: ++ response.set_rcode(dns.rcode.NXDOMAIN) ++ ++ add_signed(response.authority, soa_rrset(TLD), keys[TLD]) ++ ++ # The owner name derives zone "tld.test.", but the RRSIG signer is the ++ # malicious child zone "1b40241kforiog780n4ikscrlvetpctq.tld.test.". ++ add_signed(response.authority, child_nsec3_rrset(), keys[ATTACKER]) ++ ++ ++class VictimForgedNxdomainHandler(SignedResponseHandler): ++ """ ++ This serves the forged response for the victim's domain. ++ """ ++ ++ def match(self, qctx: QueryContext) -> bool: ++ return qctx.qname == name(VICTIM) and qctx.qtype == dns.rdatatype.A ++ ++ def respond(self, qctx: QueryContext) -> None: ++ forged_nxdomain(qctx.response, self.keys) ++ ++ ++class ChildDsHandler(SignedResponseHandler): ++ """ ++ This will spoof the response for the malicious zone when qtype is DS. ++ It is actually a validly signed DS response. ++ """ ++ ++ def match(self, qctx: QueryContext) -> bool: ++ return qctx.qname == name(ATTACKER) and qctx.qtype == dns.rdatatype.DS ++ ++ def respond(self, qctx: QueryContext) -> None: ++ response = qctx.response ++ zone = ATTACKER ++ child_key = self.keys[ATTACKER] ++ parent_key = self.keys[TLD] ++ ++ add_signed(response.answer, ds_rrset(zone, child_key), parent_key) ++ ++ ++class AttackerZoneHandler(SignedResponseHandler): ++ """ ++ Acts as the malicious authoritative name server. The zone being served ++ is the hashed label of the parent zone (tld.test). This will respond ++ for all queries qtype SOA, DNSKEY, NS at the apex. Any names below ++ the apex are answered with an NXDOMAIN with no NSEC or NSEC3 present. ++ """ ++ ++ def match(self, qctx: QueryContext) -> bool: ++ return qctx.qname.is_subdomain(name(ATTACKER)) ++ ++ def respond(self, qctx: QueryContext) -> None: ++ if qctx.qname == name(ATTACKER): ++ if qctx.qtype == dns.rdatatype.DNSKEY: ++ answer_dnskey(qctx.response, ATTACKER, self.keys[ATTACKER]) ++ elif qctx.qtype == dns.rdatatype.SOA: ++ answer_soa(qctx.response, ATTACKER, self.keys[ATTACKER]) ++ else: ++ answer_ns( ++ qctx.response, ATTACKER, f"ns.{ATTACKER}", self.keys[ATTACKER] ++ ) ++ qctx.response.additional.append(glue_rrset(f"ns.{ATTACKER}", AUTH_IP)) ++ return ++ ++ qctx.response.set_rcode(dns.rcode.NXDOMAIN) ++ add_signed(qctx.response.authority, soa_rrset(ATTACKER), self.keys[ATTACKER]) ++ ++ ++class TldZoneHandler(SignedResponseHandler): ++ """ ++ Acts as the TLD who is being used in the attack, but is not a standard ++ name server. It only responds with validly signed records for DNSKEY, SOA ++ and NS on the apex. Any names below the apex are answered with an NXDOMAIN ++ with no NSEC or NSEC3 present. ++ ++ If we turn this into a regular name server than the attack won't work. ++ The attack assumes that the adversary can inject these responses on-path. ++ """ ++ ++ def match(self, qctx: QueryContext) -> bool: ++ return qctx.qname.is_subdomain(name(TLD)) ++ ++ def respond(self, qctx: QueryContext) -> None: ++ if qctx.qname == name(TLD): ++ if qctx.qtype == dns.rdatatype.DNSKEY: ++ answer_dnskey(qctx.response, TLD, self.keys[TLD]) ++ elif qctx.qtype == dns.rdatatype.SOA: ++ answer_soa(qctx.response, TLD, self.keys[TLD]) ++ else: ++ answer_ns(qctx.response, TLD, "ns.tld.test.", self.keys[TLD]) ++ qctx.response.additional.append(glue_rrset("ns.tld.test.", AUTH_IP)) ++ return ++ ++ qctx.response.set_rcode(dns.rcode.NXDOMAIN) ++ add_signed(qctx.response.authority, soa_rrset(TLD), self.keys[TLD]) ++ ++ ++def main() -> None: ++ keys = load_keys() ++ server = AsyncDnsServer(default_aa=True) ++ server.install_response_handlers( ++ VictimForgedNxdomainHandler(keys), ++ ChildDsHandler(keys), ++ AttackerZoneHandler(keys), ++ TldZoneHandler(keys), ++ ) ++ server.run() ++ ++ ++if __name__ == "__main__": ++ main() +diff --git a/bin/tests/system/nsec3_impersonation/ns2/named.conf.j2 b/bin/tests/system/nsec3_impersonation/ns2/named.conf.j2 +new file mode 100644 +index 0000000000..2c9b0bba9e +--- /dev/null ++++ b/bin/tests/system/nsec3_impersonation/ns2/named.conf.j2 +@@ -0,0 +1,33 @@ ++// validating resolver ++ ++options { ++ query-source address 10.53.0.2; ++ notify-source 10.53.0.2; ++ transfer-source 10.53.0.2; ++ port @PORT@; ++ pid-file "named.pid"; ++ listen-on { 10.53.0.2; }; ++ listen-on-v6 { none; }; ++ recursion yes; ++ dnssec-validation yes; ++}; ++ ++controls { ++ inet 10.53.0.2 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; ++}; ++ ++include "../../_common/rndc.key"; ++ ++zone "." { ++ type hint; ++ file "../../_common/root.hint"; ++}; ++ ++zone "tld.test" { ++ type static-stub; ++ server-addresses { 10.53.0.1; }; ++}; ++ ++trust-anchors { ++ tld.test. static-key 257 3 13 "@TLD_DNSKEY@"; ++}; +diff --git a/bin/tests/system/nsec3_impersonation/tests_nsec3_impersonation.py b/bin/tests/system/nsec3_impersonation/tests_nsec3_impersonation.py +new file mode 100644 +index 0000000000..bd9bd275b6 +--- /dev/null ++++ b/bin/tests/system/nsec3_impersonation/tests_nsec3_impersonation.py +@@ -0,0 +1,152 @@ ++#!/usr/bin/python3 ++ ++# Copyright (C) Internet Systems Consortium, Inc. ("ISC") ++# ++# SPDX-License-Identifier: MPL-2.0 ++# ++# This Source Code Form is subject to the terms of the Mozilla Public ++# License, v. 2.0. If a copy of the MPL was not distributed with this ++# file, you can obtain one at https://mozilla.org/MPL/2.0/. ++# ++# See the COPYRIGHT file distributed with this work for additional ++# information regarding copyright ownership. ++ ++from pathlib import Path ++ ++import json ++ ++from cryptography.hazmat.primitives import serialization ++from cryptography.hazmat.primitives.asymmetric import ec ++ ++import dns.dnssec ++import dns.flags ++import dns.name ++import dns.rdataclass ++import dns.rdatatype ++import pytest ++ ++import isctest ++import isctest.mark ++ ++APEX_HASH = "1B40241KFORIOG780N4IKSCRLVETPCTQ" ++ATTACKER = f"{APEX_HASH.lower()}.tld.test." ++VICTIM = "victim.tld.test." ++AUTH = "10.53.0.1" ++RESOLVER = "10.53.0.2" ++ ++pytestmark = [ ++ isctest.mark.with_ecdsa_deterministic, ++ pytest.mark.extra_artifacts( ++ [ ++ "ans*/ans.run", ++ "ans*/keys.json", ++ ] ++ ), ++] ++ ++ ++def _make_key(zone): ++ private_key = ec.generate_private_key(ec.SECP256R1()) ++ dnskey = dns.dnssec.make_dnskey( ++ private_key.public_key(), ++ algorithm="ECDSAP256SHA256", ++ flags=257, ++ ) ++ ds = dns.dnssec.make_ds(dns.name.from_text(zone), dnskey, "SHA256") ++ private_pem = private_key.private_bytes( ++ encoding=serialization.Encoding.PEM, ++ format=serialization.PrivateFormat.PKCS8, ++ encryption_algorithm=serialization.NoEncryption(), ++ ).decode("ascii") ++ return { ++ "private_pem": private_pem, ++ "dnskey": dnskey.to_text(), ++ "ds": ds.to_text(), ++ } ++ ++ ++def bootstrap(): ++ zones = ["tld.test.", ATTACKER] ++ keys = {zone: _make_key(zone) for zone in zones} ++ ++ Path("ans1/keys.json").write_text(json.dumps(keys, indent=2), encoding="ascii") ++ ++ tld_dnskey = "".join(keys["tld.test."]["dnskey"].split()[3:]) ++ return {"TLD_DNSKEY": tld_dnskey} ++ ++ ++def check_dnskey_response(zone): ++ query = isctest.query.create(zone, "DNSKEY") ++ response = isctest.query.tcp(query, AUTH) ++ ++ isctest.check.noerror(response) ++ assert response.flags & dns.flags.AA ++ assert ( ++ response.get_rrset( ++ response.answer, ++ dns.name.from_text(zone), ++ dns.rdataclass.IN, ++ dns.rdatatype.DNSKEY, ++ ) ++ is not None ++ ), response ++ ++ ++def check_ds_response(zone): ++ query = isctest.query.create(zone, "DS") ++ response = isctest.query.tcp(query, AUTH) ++ ++ isctest.check.noerror(response) ++ assert response.flags & dns.flags.AA ++ assert ( ++ response.get_rrset( ++ response.answer, ++ dns.name.from_text(zone), ++ dns.rdataclass.IN, ++ dns.rdatatype.DS, ++ ) ++ is not None ++ ), response ++ ++ ++def test_attack_responses(): ++ check_dnskey_response("tld.test.") ++ check_dnskey_response(ATTACKER) ++ check_ds_response(ATTACKER) ++ ++ query = isctest.query.create(VICTIM, "A") ++ response = isctest.query.tcp(query, AUTH) ++ ++ isctest.check.nxdomain(response) ++ assert response.flags & dns.flags.AA ++ ++ nsec3_owner = dns.name.from_text(f"{APEX_HASH}.tld.test.") ++ nsec3 = response.get_rrset( ++ response.authority, ++ nsec3_owner, ++ dns.rdataclass.IN, ++ dns.rdatatype.NSEC3, ++ ) ++ rrsig = response.get_rrset( ++ response.authority, ++ nsec3_owner, ++ dns.rdataclass.IN, ++ dns.rdatatype.RRSIG, ++ covers=dns.rdatatype.NSEC3, ++ ) ++ ++ assert nsec3 is not None, response ++ assert rrsig is not None, response ++ assert rrsig[0].signer == dns.name.from_text(ATTACKER) ++ ++ ++def test_nsec3_impersonation(): ++ """ ++ Reproducer for #5874: ++ F-006 DNSSEC Validation Bypass NSEC3 Apex Hash Label Parent Impersonation ++ """ ++ query = isctest.query.create(VICTIM, "A") ++ response = isctest.query.tcp(query, RESOLVER) ++ ++ isctest.check.noadflag(response) ++ isctest.check.servfail(response) +diff --git a/bin/tests/system/qmin/ans2/ans.py b/bin/tests/system/qmin/ans2/ans.py +index d372c2003b..9343fbc8ef 100755 +--- a/bin/tests/system/qmin/ans2/ans.py ++++ b/bin/tests/system/qmin/ans2/ans.py +@@ -15,11 +15,12 @@ import sys + import signal + import socket + import select +-from datetime import datetime, timedelta + import time +-import functools + +-import dns, dns.message, dns.query, dns.flags ++import dns ++import dns.message ++import dns.query ++import dns.flags + from dns.rdatatype import * + from dns.rdataclass import * + from dns.rcode import * +@@ -432,9 +433,9 @@ else: + while running: + try: + inputready, outputready, exceptready = select.select(input, [], []) +- except select.error as e: ++ except select.error: + break +- except socket.error as e: ++ except socket.error: + break + except KeyboardInterrupt: + break +diff --git a/bin/tests/system/qmin/ans3/ans.py b/bin/tests/system/qmin/ans3/ans.py +index b5ae73c3fa..4e7250790f 100755 +--- a/bin/tests/system/qmin/ans3/ans.py ++++ b/bin/tests/system/qmin/ans3/ans.py +@@ -15,11 +15,12 @@ import sys + import signal + import socket + import select +-from datetime import datetime, timedelta + import time +-import functools + +-import dns, dns.message, dns.query, dns.flags ++import dns ++import dns.message ++import dns.query ++import dns.flags + from dns.rdatatype import * + from dns.rdataclass import * + from dns.rcode import * +@@ -261,9 +262,9 @@ else: + while running: + try: + inputready, outputready, exceptready = select.select(input, [], []) +- except select.error as e: ++ except select.error: + break +- except socket.error as e: ++ except socket.error: + break + except KeyboardInterrupt: + break +diff --git a/bin/tests/system/qmin/ans4/ans.py b/bin/tests/system/qmin/ans4/ans.py +index 517217aec1..2d5556daff 100755 +--- a/bin/tests/system/qmin/ans4/ans.py ++++ b/bin/tests/system/qmin/ans4/ans.py +@@ -15,11 +15,12 @@ import sys + import signal + import socket + import select +-from datetime import datetime, timedelta + import time +-import functools + +-import dns, dns.message, dns.query, dns.flags ++import dns ++import dns.message ++import dns.query ++import dns.flags + from dns.rdatatype import * + from dns.rdataclass import * + from dns.rcode import * +@@ -320,9 +321,9 @@ else: + while running: + try: + inputready, outputready, exceptready = select.select(input, [], []) +- except select.error as e: ++ except select.error: + break +- except socket.error as e: ++ except socket.error: + break + except KeyboardInterrupt: + break +diff --git a/bin/tests/system/resolver/ans10/ans.py b/bin/tests/system/resolver/ans10/ans.py +index 6e95dbbfc6..d637c63e5a 100644 +--- a/bin/tests/system/resolver/ans10/ans.py ++++ b/bin/tests/system/resolver/ans10/ans.py +@@ -15,11 +15,11 @@ import sys + import signal + import socket + import select +-from datetime import datetime, timedelta +-import time +-import functools + +-import dns, dns.message, dns.query, dns.flags ++import dns ++import dns.message ++import dns.query ++import dns.flags + from dns.rdatatype import * + from dns.rdataclass import * + from dns.rcode import * +@@ -128,9 +128,9 @@ else: + while running: + try: + inputready, outputready, exceptready = select.select(input, [], []) +- except select.error as e: ++ except select.error: + break +- except socket.error as e: ++ except socket.error: + break + except KeyboardInterrupt: + break +-- +2.55.0 + diff --git a/bind-9.18-CVE-2026-10723.patch b/bind-9.18-CVE-2026-10723.patch new file mode 100644 index 0000000..a519b38 --- /dev/null +++ b/bind-9.18-CVE-2026-10723.patch @@ -0,0 +1,67 @@ +From 608026780a43abe5b23a9af3af21808369032158 Mon Sep 17 00:00:00 2001 +From: Evan Hunt +Date: Thu, 21 May 2026 14:41:55 -0700 +Subject: [PATCH] Check NSEC3 signer matches the owning zone + +When validating NSEC3 records, reject any signature whose signer field +does not match the zone owning the NSEC3. + +This ensures that a child zone cannot impersonate its parent and forge +NXDOMAIN responses for sibling domains. + +Fixes: isc-projects/bind9#5874 +(cherry picked from commit 6e5066bb1f0f12d090e8707adb7d6ccf74f8012b) +(cherry picked from commit c9cb6a5e24e43489cf3fd4d4cc2193b6a74499cb) +--- + lib/dns/dnssec.c | 19 +++++++++++++++++-- + lib/isc/result.c | 2 +- + 2 files changed, 18 insertions(+), 3 deletions(-) + +diff --git a/lib/dns/dnssec.c b/lib/dns/dnssec.c +index b12529b5d5..9b9b1f2bb2 100644 +--- a/lib/dns/dnssec.c ++++ b/lib/dns/dnssec.c +@@ -424,10 +424,25 @@ dns_dnssec_verify(const dns_name_t *name, dns_rdataset_t *set, dst_key_t *key, + } + + /* +- * NS, SOA and DNSKEY records are signed by their owner. +- * DS records are signed by the parent. ++ * NS, SOA and DNSKEY records are signed by their owners. ++ * NSEC3 records are signed by the apex, exactly one level up ++ * from their owner names. ++ * DS records are signed by the parent zone. + */ + switch (set->type) { ++ case dns_rdatatype_nsec3: { ++ dns_name_t apex = DNS_NAME_INITEMPTY; ++ labels = dns_name_countlabels(name); ++ if (labels <= 1) { ++ inc_stat(dns_dnssecstats_fail); ++ return DNS_R_INVALIDNSEC3; ++ } ++ dns_name_split(name, labels - 1, NULL, &apex); ++ if (!dns_name_equal(&apex, &sig.signer)) { ++ inc_stat(dns_dnssecstats_fail); ++ return DNS_R_SIGINVALID; ++ } ++ } break; + case dns_rdatatype_ns: + case dns_rdatatype_soa: + case dns_rdatatype_dnskey: +diff --git a/lib/isc/result.c b/lib/isc/result.c +index 83e8cfeed7..b76c3d1f7b 100644 +--- a/lib/isc/result.c ++++ b/lib/isc/result.c +@@ -198,7 +198,7 @@ static const char *description[ISC_R_NRESULTS] = { + [DNS_R_COVERINGNSEC] = "covering NSEC record returned", + [DNS_R_MXISADDRESS] = "MX is an address", + [DNS_R_DUPLICATE] = "duplicate query", +- [DNS_R_INVALIDNSEC3] = "invalid NSEC3 owner name (wildcard)", ++ [DNS_R_INVALIDNSEC3] = "invalid NSEC3 owner name", + [DNS_R_NOTPRIMARY] = "not primary", + [DNS_R_BROKENCHAIN] = "broken trust chain", + [DNS_R_EXPIRED] = "expired", +-- +2.55.0 + diff --git a/bind.spec b/bind.spec index 0c9e439..6ac624f 100644 --- a/bind.spec +++ b/bind.spec @@ -160,6 +160,9 @@ Patch43: bind-9.18-CVE-2026-11622.patch # https://gitlab.isc.org/isc-projects/bind9/commit/58812f64bf14b3930f5bb90a2d0e1f59bc8260b4 Patch44: bind-9.18-CVE-2026-11721.patch Patch45: bind-9.18-CVE-2026-11721-test.patch +# https://gitlab.isc.org/isc-projects/bind9/commit/348fd47f7636f610a39ba98427fcacab8e62389b +Patch46: bind-9.18-CVE-2026-10723.patch +Patch47: bind-9.18-CVE-2026-10723-test.patch %{?systemd_ordering} # https://fedoraproject.org/wiki/Changes/RPMSuportForSystemdSysusers @@ -966,6 +969,7 @@ fi; - Record ordering based unexpected exit with CNAME or DNAME (CVE-2026-12617) - Potential memory usage beyond configured limits (CVE-2026-11622) - Cache poisoning via label count discrepancy, RRSIG, wildcards (CVE-2026-11721) +- Incorrect acceptance of NSEC3 records (CVE-2026-10723) * Wed Jun 17 2026 Petr Menšík - 32:9.18.50-1 - Update to 9.18.50 (rhbz#2489833)