From: Ondřej Surý Date: Fri, 20 Mar 2026 07:43:28 +0000 (+0100) Subject: Add regression test for RFC 3645 Section 4.1.1 duplicate TKEY name X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=80654250ee551167f54e16603e05d0aa38f70c0a;p=thirdparty%2Fbind9.git Add regression test for RFC 3645 Section 4.1.1 duplicate TKEY name Add 'tkeyname' command to nsupdate to allow specifying a fixed TKEY name instead of the default random one. This is used by the test to send two GSS-API TKEY negotiations with the same name. After a successful GSS-API TKEY negotiation via nsupdate -g, a second attempt with the same TKEY name must be rejected with BADKEY (error=17), not BADNAME (error=20). --- diff --git a/bin/nsupdate/nsupdate.c b/bin/nsupdate/nsupdate.c index a51441e2a14..1324e11305c 100644 --- a/bin/nsupdate/nsupdate.c +++ b/bin/nsupdate/nsupdate.c @@ -209,6 +209,7 @@ static isc_sockaddr_t *kserver = NULL; static char *realm = NULL; static char servicename[DNS_NAME_FORMATSIZE]; static dns_name_t *keyname; +static char *user_tkey_name = NULL; typedef struct nsu_gssinfo { dns_message_t *msg; isc_sockaddr_t *addr; @@ -2324,6 +2325,21 @@ do_next_command(char *cmdline) { usegsstsig = true; #else /* HAVE_GSSAPI */ fprintf(stderr, "gsstsig not supported\n"); +#endif /* HAVE_GSSAPI */ + return STATUS_MORE; + } + if (strcasecmp(word, "tkeyname") == 0) { +#if HAVE_GSSAPI + if (user_tkey_name != NULL) { + isc_mem_free(isc_g_mctx, user_tkey_name); + } + + word = nsu_strsep(&cmdline, " \t\r\n"); + if (word != NULL && *word != 0) { + user_tkey_name = isc_mem_strdup(isc_g_mctx, word); + } +#else /* HAVE_GSSAPI */ + fprintf(stderr, "gsstsig not supported\n"); #endif /* HAVE_GSSAPI */ return STATUS_MORE; } @@ -3089,10 +3105,14 @@ start_gssrequest(dns_name_t *primary) { keyname = dns_fixedname_initname(&fkname); - isc_nonce_buf(&val, sizeof(val)); - - result = snprintf(mykeystr, sizeof(mykeystr), "%u.sig-%s", val, - namestr); + if (user_tkey_name != NULL) { + result = snprintf(mykeystr, sizeof(mykeystr), "%s", + user_tkey_name); + } else { + isc_nonce_buf(&val, sizeof(val)); + result = snprintf(mykeystr, sizeof(mykeystr), "%u.sig-%s", val, + namestr); + } RUNTIME_CHECK(result <= sizeof(mykeystr)); isc_buffer_init(&buf, mykeystr, strlen(mykeystr)); @@ -3438,6 +3458,9 @@ cleanup(void) { if (realm != NULL) { isc_mem_free(isc_g_mctx, realm); } + if (user_tkey_name != NULL) { + isc_mem_free(isc_g_mctx, user_tkey_name); + } if (dns_name_dynamic(&tmpzonename)) { dns_name_free(&tmpzonename, isc_g_mctx); } diff --git a/bin/tests/system/tsiggss/tests_tkey_duplicate.py b/bin/tests/system/tsiggss/tests_tkey_duplicate.py new file mode 100644 index 00000000000..c38c358f660 --- /dev/null +++ b/bin/tests/system/tsiggss/tests_tkey_duplicate.py @@ -0,0 +1,92 @@ +# 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. + +""" +Test that a second TKEY query with the same name is rejected with BADKEY. + +RFC 3645 Section 4.1.1: if a non-expired key already exists for a +given name, the server must reject the query with BADKEY. + +This test uses nsupdate -g with a fixed TKEY name (via the 'tkeyname' +command) to complete a real GSS-API TKEY negotiation, then runs +nsupdate -g again with the same TKEY name and verifies that the +server rejects it with BADKEY. +""" + +import os +import subprocess + +import pytest + +import isctest + +pytestmark = pytest.mark.extra_artifacts( + [ + "nsupdate.out*", + "ns1/K*", + "ns1/_default.tsigkeys", + "ns1/example.nil.db", + "ns1/example.nil.db.jnl", + ] +) + +TKEY_NAME = "duptest.sig-example.nil." + + +def run_nsupdate_gss(ns1, tkey_name, record_name, record_value): + """Run nsupdate -g with a fixed TKEY name. + + Returns the subprocess result. + """ + nsupdate = isctest.vars.ALL["NSUPDATE"] + port = ns1.ports.dns + + update_cmd = ( + f"gsstsig\n" + f"tkeyname {tkey_name}\n" + f"server {ns1.ip} {port}\n" + f"zone example.nil.\n" + f"update add {record_name} 86400 A {record_value}\n" + f"send\n" + ) + + os.environ["KRB5CCNAME"] = f"FILE:{os.getcwd()}/ns1/administrator.ccache" + + return subprocess.run( + [nsupdate, "-d"], + input=update_cmd, + capture_output=True, + text=True, + timeout=30, + check=False, + ) + + +def test_tkey_duplicate_name_rejected(ns1): + """Second TKEY query for an existing key name must return BADKEY. + + RFC 3645 Section 4.1.1: if a non-expired TSIG key exists for the + name, the server must reject a new TKEY query for that name. + """ + # Step 1: Complete a real GSS-API TKEY negotiation with a fixed name + result = run_nsupdate_gss(ns1, TKEY_NAME, "duptest1.example.nil.", "10.53.0.99") + assert ( + result.returncode == 0 + ), f"first nsupdate -g failed (rc={result.returncode}):\n{result.stderr}" + + # Step 2: Try the same TKEY name again — must fail + result = run_nsupdate_gss(ns1, TKEY_NAME, "duptest2.example.nil.", "10.53.0.98") + assert ( + result.returncode != 0 + ), "second nsupdate -g with duplicate TKEY name should have failed" + assert ( + "BADKEY" in result.stderr or "REFUSED" in result.stderr + ), f"expected BADKEY or REFUSED in error output, got:\n{result.stderr}"