]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Add regression test for RFC 3645 Section 4.1.1 duplicate TKEY name
authorOndřej Surý <ondrej@sury.org>
Fri, 20 Mar 2026 07:43:28 +0000 (08:43 +0100)
committerOndřej Surý <ondrej@sury.org>
Wed, 1 Apr 2026 05:04:40 +0000 (07:04 +0200)
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).

bin/nsupdate/nsupdate.c
bin/tests/system/tsiggss/tests_tkey_duplicate.py [new file with mode: 0644]

index a51441e2a14b1860ea0ad22ef6e7af4748b74f77..1324e11305cbb83816ee4442b2bfaf613fac4721 100644 (file)
@@ -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 (file)
index 0000000..c38c358
--- /dev/null
@@ -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}"