From: Ondřej Surý Date: Tue, 24 Feb 2026 18:20:14 +0000 (+0100) Subject: Add system test using SIG(0) and ACL matching X-Git-Tag: v9.21.20~2^2~1 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=613a93478ba7c406a9f111b3dad0fe1b36ee8a8d;p=thirdparty%2Fbind9.git Add system test using SIG(0) and ACL matching This adds a system test to verify that asynchronous SIG(0) validation correctly retains the ACL environment and network addresses of the caller, preventing unauthorized ACL bypass when evaluating match-clients and match-destinations. --- diff --git a/bin/tests/system/sig0/ns1/named.conf.j2 b/bin/tests/system/sig0/ns1/named.conf.j2 new file mode 100644 index 00000000000..724a40c58cd --- /dev/null +++ b/bin/tests/system/sig0/ns1/named.conf.j2 @@ -0,0 +1,41 @@ +/* + * 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. + */ + +options { + query-source address 10.53.0.1; + notify-source 10.53.0.1; + transfer-source 10.53.0.1; + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.1; }; + listen-on-v6 { none; }; + recursion no; + notify no; +}; + +key rndc_key { + secret "1234abcd8765"; + algorithm @DEFAULT_HMAC@; +}; + +controls { + inet 10.53.0.2 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; +}; + +view "v1" { + match-clients { any; }; + zone "." { + type hint; + file "/dev/null"; + }; +}; diff --git a/bin/tests/system/sig0/setup.sh b/bin/tests/system/sig0/setup.sh new file mode 100644 index 00000000000..64a8c3a4814 --- /dev/null +++ b/bin/tests/system/sig0/setup.sh @@ -0,0 +1,17 @@ +#!/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 + +key=$($KEYGEN -q -a RSASHA256 -b 2048 sig0.) diff --git a/bin/tests/system/sig0/tests_sig0.py b/bin/tests/system/sig0/tests_sig0.py new file mode 100644 index 00000000000..500d2b5eab9 --- /dev/null +++ b/bin/tests/system/sig0/tests_sig0.py @@ -0,0 +1,119 @@ +# 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. + +import base64 +import glob +import os +import struct +import time + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import padding, rsa + +import dns.flags +import dns.message +import dns.name +import dns.rdata +import dns.rdataclass +import dns.rdatatype +import dns.renderer +import dns.rrset + +import isctest + + +def load_bind_private_key(filename): + """Parses a BIND 9 .private key file.""" + with open(filename, "r", encoding="utf-8") as f: + lines = f.readlines() + + data = {} + for line in lines: + if ":" in line: + key, value = line.split(":", 1) + data[key.strip()] = value.strip() + + def b64int(k): + return int.from_bytes(base64.b64decode(data[k]), byteorder="big") + + rsa_key = rsa.RSAPrivateNumbers( + p=b64int("Prime1"), + q=b64int("Prime2"), + d=b64int("PrivateExponent"), + dmp1=b64int("Exponent1"), + dmq1=b64int("Exponent2"), + iqmp=b64int("Coefficient"), + public_numbers=rsa.RSAPublicNumbers( + e=b64int("PublicExponent"), n=b64int("Modulus") + ), + ).private_key(default_backend()) + + return rsa_key + + +def make_sig0_query(key_file, key_name_str): + private_key = load_bind_private_key(key_file) + + qname = dns.name.from_text(".") + query = dns.message.make_query(qname, dns.rdatatype.SOA) + query.flags |= dns.flags.RD + + # Render message to bytes (needed for signing) + renderer = dns.renderer.Renderer() + query.to_wire(renderer) + msg_bytes = renderer.get_wire() + + # SIG(0) Constants + basename = os.path.basename(key_file) + key_tag = int(basename.split("+")[2].split(".")[0]) + + now = int(time.time()) + expiration = now + 3600 + inception = now - 3600 + signer_name = dns.name.from_text(key_name_str) + + # Construct SIG RDATA header (0=SIG(0), 8=RSASHA256, 0=Labels) + sig_rdata_header = struct.pack( + "!HBBIIIH", 0, 8, 0, 0, expiration, inception, key_tag + ) + + sig_rdata_pre_sig = sig_rdata_header + signer_name.to_wire() + + # Sign: ( SIG RDATA sans signature ) + ( Message ) + signature = private_key.sign( + sig_rdata_pre_sig + msg_bytes, padding.PKCS1v15(), hashes.SHA256() + ) + + # Create the SIG RR + full_sig_rdata = sig_rdata_pre_sig + signature + sig_rr = dns.rdata.from_wire( + dns.rdataclass.ANY, + dns.rdatatype.SIG, + full_sig_rdata, + 0, + len(full_sig_rdata), + ) + sig_rrset = dns.rrset.from_rdata(qname, 0, sig_rr) + query.additional.append(sig_rrset) + + return query + + +def test_sig0_acl_bypass(): + key_files = glob.glob("Ksig0.+*.private") + assert len(key_files) == 1 + + query = make_sig0_query(key_files[0], "sig0.") + + # Send the query + res = isctest.query.tcp(query, "10.53.0.1") + isctest.check.servfail(res)