From 613a93478ba7c406a9f111b3dad0fe1b36ee8a8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Sur=C3=BD?= Date: Tue, 24 Feb 2026 19:20:14 +0100 Subject: [PATCH 1/2] 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. --- bin/tests/system/sig0/ns1/named.conf.j2 | 41 ++++++++ bin/tests/system/sig0/setup.sh | 17 ++++ bin/tests/system/sig0/tests_sig0.py | 119 ++++++++++++++++++++++++ 3 files changed, 177 insertions(+) create mode 100644 bin/tests/system/sig0/ns1/named.conf.j2 create mode 100644 bin/tests/system/sig0/setup.sh create mode 100644 bin/tests/system/sig0/tests_sig0.py 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 0000000000..724a40c58c --- /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 0000000000..64a8c3a481 --- /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 0000000000..500d2b5eab --- /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) From b4b81deed9930fdb59cfbf8179218621c812497b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Sur=C3=BD?= Date: Fri, 20 Feb 2026 09:46:32 +0100 Subject: [PATCH 2/2] Fix stack Use-After-Return in SIG(0) handling The asynchronous SIG(0) handling improperly used srcaddr, and dstaddr from the caller's stack and didn't attach to aclenv. This could possibly lead to ACL bypass as an invalid srcaddr could be matched or possible assertion failure if the ACL environment would change between the initial call and the SIG(0) processing due to the server reconfiguration. This has been fixed. --- bin/named/server.c | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/bin/named/server.c b/bin/named/server.c index 25eb888e7a..6a9a39846d 100644 --- a/bin/named/server.c +++ b/bin/named/server.c @@ -24,6 +24,8 @@ #include #include +#include + #ifdef HAVE_DNSTAP #include #endif @@ -280,10 +282,10 @@ struct zonelistentry { * asynchronously. */ typedef struct matching_view_ctx { - isc_netaddr_t *srcaddr; - isc_netaddr_t *destaddr; + isc_netaddr_t srcaddr; + isc_netaddr_t destaddr; dns_message_t *message; - dns_aclenv_t *env; + dns_aclenv_t *aclenv; ns_server_t *sctx; isc_loop_t *loop; isc_job_cb cb; @@ -9299,6 +9301,8 @@ get_matching_view_done(void *cbarg) { mvctx->cb(mvctx->cbarg); + dns_aclenv_detach(&mvctx->aclenv); + if (mvctx->quota_result == ISC_R_SUCCESS) { isc_quota_release(&mvctx->sctx->sig0checksquota); } @@ -9340,10 +9344,10 @@ get_matching_view_continue(void *cbarg, isc_result_t result) { tsig = dns_tsigkey_identity(mvctx->message->tsigkey); } - if (dns_acl_allowed(mvctx->srcaddr, tsig, mvctx->view->matchclients, - mvctx->env) && - dns_acl_allowed(mvctx->destaddr, tsig, - mvctx->view->matchdestinations, mvctx->env) && + if (dns_acl_allowed(&mvctx->srcaddr, tsig, mvctx->view->matchclients, + mvctx->aclenv) && + dns_acl_allowed(&mvctx->destaddr, tsig, + mvctx->view->matchdestinations, mvctx->aclenv) && !(mvctx->view->matchrecursiveonly && (mvctx->message->flags & DNS_MESSAGEFLAG_RD) == 0)) { @@ -9415,9 +9419,9 @@ get_matching_view(isc_netaddr_t *srcaddr, isc_netaddr_t *destaddr, matching_view_ctx_t *mvctx = isc_mem_get(message->mctx, sizeof(*mvctx)); *mvctx = (matching_view_ctx_t){ - .srcaddr = srcaddr, - .destaddr = destaddr, - .env = env, + .srcaddr = *srcaddr, + .destaddr = *destaddr, + .aclenv = dns_aclenv_ref(env), .cb = cb, .cbarg = cbarg, .sigresult = sigresult,