]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Add system test using SIG(0) and ACL matching
authorOndřej Surý <ondrej@isc.org>
Tue, 24 Feb 2026 18:20:14 +0000 (19:20 +0100)
committerMichał Kępień <michal@isc.org>
Fri, 13 Mar 2026 12:47:17 +0000 (13:47 +0100)
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.

bin/tests/system/sig0/ns1/named.conf.j2 [new file with mode: 0644]
bin/tests/system/sig0/setup.sh [new file with mode: 0644]
bin/tests/system/sig0/tests_sig0.py [new file with mode: 0644]

diff --git a/bin/tests/system/sig0/ns1/named.conf.j2 b/bin/tests/system/sig0/ns1/named.conf.j2
new file mode 100644 (file)
index 0000000..724a40c
--- /dev/null
@@ -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 (file)
index 0000000..64a8c3a
--- /dev/null
@@ -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 (file)
index 0000000..500d2b5
--- /dev/null
@@ -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)