public inbox for git-commits@fedoraproject.org
help / color / mirror / Atom feed
* [rpms/bind] f45: Incorrect acceptance of NSEC3 records (CVE-2026-10723)
@ 2026-08-26 8:34
0 siblings, 0 replies; only message in thread
From: @ 2026-08-26 8:34 UTC (permalink / raw)
To: git-commits
A new commit has been pushed.
Repo : rpms/bind
Branch : f45
Commit : 7e9f5e27cccd5bc7d917505c4ab025e6c36aa11e
Author : Petr Menšík <pemensik@redhat.com>
Date : 2026-08-25T13:22:35+02:00
Stats : +924/-0 in 3 file(s)
URL : https://src.fedoraproject.org/rpms/bind/c/7e9f5e27cccd5bc7d917505c4ab025e6c36aa11e?branch=f45
Log:
Incorrect acceptance of NSEC3 records (CVE-2026-10723)
[9.18] [CVE-2026-10723] sec: usr: Correct verification of NSEC3 signer name
BIND 9 accepted child-zone NSEC3 records where the first label equals the hash of the parent zone as valid parent-zone closest encloser proofs. This has been fixed.
ISC thanks Qifan Zhang of Palo Alto Networks for reporting the issue.
Closes isc-projects/bind9#5874
---
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 <each@isc.org>
+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 <each@isc.org>
+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 97c1f00..08b85ad 100644
--- a/bind.spec
+++ b/bind.spec
@@ -176,6 +176,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
^ permalink raw reply related [flat|nested] only message in thread
only message in thread, other threads:[~2026-08-26 8:34 UTC | newest]
Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-08-26 8:34 [rpms/bind] f45: Incorrect acceptance of NSEC3 records (CVE-2026-10723)
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox