Commit 021ea1508f for bind

commit 021ea1508f46b482c7186a4be70300c2596735b3
Author: Nicki Křížek <nicki@isc.org>
Date:   Fri Oct 2 14:02:24 2026 +0000

    Test an offline KSK and a ZSK with the same key tag

    The keys are pre-generated, as finding a colliding pair takes a
    brute-force search.

    Assisted-by: Claude:claude-opus-5-5

diff --git a/REUSE.toml b/REUSE.toml
index 2602a08198..d5a03a653b 100644
--- a/REUSE.toml
+++ b/REUSE.toml
@@ -14,11 +14,14 @@ path = [
 	"**/**.good",
 	"**/**.key",
 	"**/**.key.in",
+	"**/**.key.j2",
 	"**/**.keytab",
 	"**/**.pem",
 	"**/**.private",
+	"**/**.private.j2",
 	"**/**.raw",
 	"**/**.saved",
+	"**/**.state.j2",
 	"**/**.zonelist",
 	"**/**dig.out**",
 	"**/root.hint*",
diff --git a/bin/tests/system/ksr/ns1/Kcollision.test.+013+59801.key.j2 b/bin/tests/system/ksr/ns1/Kcollision.test.+013+59801.key.j2
new file mode 100644
index 0000000000..cf9fe80773
--- /dev/null
+++ b/bin/tests/system/ksr/ns1/Kcollision.test.+013+59801.key.j2
@@ -0,0 +1,5 @@
+; This is a zone-signing key, keyid 59801, for collision.test.
+; Created: 20261002135138 (Fri Oct  2 13:51:38 2026)
+; Publish: 20261002114638 (Fri Oct  2 11:46:38 2026)
+; Activate: 20261002135138 (Fri Oct  2 13:51:38 2026)
+collision.test. 3600 IN DNSKEY 256 3 13 Y3DnyQIs45OFmo/RsU26hphYm/6o0kJi 5vpXl4+Lkw4pEE4BhwXbPlg6b3cGNB4T GDv/1c8Xr1L65OKwcvwGPQ==
diff --git a/bin/tests/system/ksr/ns1/Kcollision.test.+013+59801.private.j2 b/bin/tests/system/ksr/ns1/Kcollision.test.+013+59801.private.j2
new file mode 100644
index 0000000000..ef93c85abe
--- /dev/null
+++ b/bin/tests/system/ksr/ns1/Kcollision.test.+013+59801.private.j2
@@ -0,0 +1,6 @@
+Private-key-format: v1.3
+Algorithm: 13 (ECDSAP256SHA256)
+PrivateKey: NxYzgdqUTRx1DnuItRAvOGQZqAKmUfpowxI/O4xkL3I=
+Created: 20261002135138
+Publish: 20261002114638
+Activate: 20261002135138
diff --git a/bin/tests/system/ksr/ns1/Kcollision.test.+013+59801.state.j2 b/bin/tests/system/ksr/ns1/Kcollision.test.+013+59801.state.j2
new file mode 100644
index 0000000000..f34ff650f5
--- /dev/null
+++ b/bin/tests/system/ksr/ns1/Kcollision.test.+013+59801.state.j2
@@ -0,0 +1,9 @@
+; This is the state of key 59801, for collision.test.
+Algorithm: 13
+Length: 256
+Lifetime: 0
+KSK: no
+ZSK: yes
+Generated: 20261002135138 (Fri Oct  2 13:51:38 2026)
+Published: 20261002114638 (Fri Oct  2 11:46:38 2026)
+Active: 20261002135138 (Fri Oct  2 13:51:38 2026)
diff --git a/bin/tests/system/ksr/ns1/named.conf.j2 b/bin/tests/system/ksr/ns1/named.conf.j2
index f0b586c515..1dd240f972 100644
--- a/bin/tests/system/ksr/ns1/named.conf.j2
+++ b/bin/tests/system/ksr/ns1/named.conf.j2
@@ -87,6 +87,14 @@ dnssec-policy "fast" {
 	parent-propagation-delay 5;
 };

+dnssec-policy "collision" {
+	offline-ksk yes;
+	keys {
+		ksk lifetime unlimited algorithm ecdsap256sha256;
+		zsk lifetime unlimited algorithm ecdsap256sha256;
+	};
+};
+
 dnssec-policy "invalid-skr" {
 	offline-ksk yes;
 	keys {
diff --git a/bin/tests/system/ksr/ns1/offline/Kcollision.test.+013+59801.key.j2 b/bin/tests/system/ksr/ns1/offline/Kcollision.test.+013+59801.key.j2
new file mode 100644
index 0000000000..d8c2e2cefe
--- /dev/null
+++ b/bin/tests/system/ksr/ns1/offline/Kcollision.test.+013+59801.key.j2
@@ -0,0 +1,6 @@
+; This is a key-signing key, keyid 59801, for collision.test.
+; Created: 20261002135138 (Fri Oct  2 13:51:38 2026)
+; Publish: 20261002114638 (Fri Oct  2 11:46:38 2026)
+; Activate: 20261002135138 (Fri Oct  2 13:51:38 2026)
+; SyncPublish: 20261003115138 (Sat Oct  3 11:51:38 2026)
+collision.test. 3600 IN DNSKEY 257 3 13 JOazOBxTLZ5WaN4G9XiHpF89zEyKXfF7 Gaf6IV0gMazEGet++f2s45R+h/OmYAfR 6V9t3B4QMJ7hS+8NT8Lg0A==
diff --git a/bin/tests/system/ksr/ns1/offline/Kcollision.test.+013+59801.private.j2 b/bin/tests/system/ksr/ns1/offline/Kcollision.test.+013+59801.private.j2
new file mode 100644
index 0000000000..d83ac28626
--- /dev/null
+++ b/bin/tests/system/ksr/ns1/offline/Kcollision.test.+013+59801.private.j2
@@ -0,0 +1,7 @@
+Private-key-format: v1.3
+Algorithm: 13 (ECDSAP256SHA256)
+PrivateKey: +ZKH/ZSHNZbYn6dst6ZMUJePTYd2KgGCPYqqBwPJkaA=
+Created: 20261002135138
+Publish: 20261002114638
+Activate: 20261002135138
+SyncPublish: 20261003115138
diff --git a/bin/tests/system/ksr/ns1/offline/Kcollision.test.+013+59801.state.j2 b/bin/tests/system/ksr/ns1/offline/Kcollision.test.+013+59801.state.j2
new file mode 100644
index 0000000000..04be5a59eb
--- /dev/null
+++ b/bin/tests/system/ksr/ns1/offline/Kcollision.test.+013+59801.state.j2
@@ -0,0 +1,10 @@
+; This is the state of key 59801, for collision.test.
+Algorithm: 13
+Length: 256
+Lifetime: 0
+KSK: yes
+ZSK: no
+Generated: 20261002135138 (Fri Oct  2 13:51:38 2026)
+Published: 20261002114638 (Fri Oct  2 11:46:38 2026)
+Active: 20261002135138 (Fri Oct  2 13:51:38 2026)
+PublishCDS: 20261003115138 (Sat Oct  3 11:51:38 2026)
diff --git a/bin/tests/system/ksr/ns1/setup.sh b/bin/tests/system/ksr/ns1/setup.sh
index 482bafa9e8..3c69533cbc 100644
--- a/bin/tests/system/ksr/ns1/setup.sh
+++ b/bin/tests/system/ksr/ns1/setup.sh
@@ -16,7 +16,6 @@

 # Key directories
 mkdir keydir
-mkdir offline

 # Zone files
 cp template.db.in common.test.db
@@ -29,3 +28,4 @@ cp template.db.in two-tone.test.db
 cp template.db.in ksk-roll.test.db
 cp template.db.in invalid-skr.test.db
 cp template.db.in fast.test.db
+cp template.db.in collision.test.db
diff --git a/bin/tests/system/ksr/tests_ksr.py b/bin/tests/system/ksr/tests_ksr.py
index c9752b71b8..f8db9a81cf 100644
--- a/bin/tests/system/ksr/tests_ksr.py
+++ b/bin/tests/system/ksr/tests_ksr.py
@@ -10,6 +10,7 @@
 # information regarding copyright ownership.

 from datetime import timedelta
+from re import compile as Re

 import os
 import re
@@ -18,7 +19,7 @@ import time

 import pytest

-from isctest.algorithms import Algorithm
+from isctest.algorithms import ECDSAP256SHA256, Algorithm
 from isctest.kasp import KeyTimingMetadata
 from rollover.common import TIMEDELTA

@@ -27,6 +28,7 @@ import isctest
 pytestmark = pytest.mark.extra_artifacts(
     [
         "K*",
+        "collision.test.*",
         "common.test.*",
         "fast.test.*",
         "future.test.*",
@@ -40,6 +42,7 @@ pytestmark = pytest.mark.extra_artifacts(
         "ns1/K*",
         "ns1/_default.nzd",
         "ns1/_default.nzf",
+        "ns1/collision.test.*",
         "ns1/common.test.db",
         "ns1/common.test.db.jbk",
         "ns1/common.test.db.signed",
@@ -1478,6 +1481,74 @@ def test_ksr_fast(ns1):
     isctest.kasp.check_subdomain(ns1, zone, ksks, zsks, offline_ksk=True)


+def test_ksr_keytag_collision(ns1):
+    # The offline KSK and the ZSK are generated by different parties, so
+    # their key tags may collide. Making them collide takes a brute-force
+    # search, so the keys are pre-generated.
+    zone = "collision.test"
+    policy = "collision"
+    alg = ECDSAP256SHA256
+    n = 1
+
+    ksks = isctest.kasp.keydir_to_keylist(zone, "ns1/offline")
+    zsks = isctest.kasp.keydir_to_keylist(zone, "ns1")
+    assert len(ksks) == 1
+    assert len(zsks) == 1
+    assert ksks[0].tag == zsks[0].tag
+
+    check_keys(ksks, None, CONFIG, alg.dst, alg.bits)
+    check_keys(zsks, None, CONFIG, alg.dst, alg.bits)
+
+    now = KeyTimingMetadata.now()
+    until = now + timedelta(days=365)
+    ksr_fname = f"{zone}.ksr.{n}"
+    ksr(
+        zone,
+        policy,
+        "request",
+        options=f"-K ns1 -i {now} -e +1y",
+        to_file=ksr_fname,
+    )
+    check_keysigningrequest(ksr_fname, zsks, now, until)
+
+    refresh = -432000  # 5 days
+    skr_fname = f"{zone}.skr.{n}"
+    ksr(
+        zone,
+        policy,
+        "sign",
+        options=f"-K ns1/offline -f {ksr_fname} -i {now} -e +1y",
+        to_file=skr_fname,
+    )
+    check_signedkeyresponse(skr_fname, CONFIG, zone, ksks, zsks, now, until, refresh)
+
+    # Without an SKR, the rekey that follows loading the zone fails.
+    with ns1.watch_log_from_here() as watcher:
+        ns1.rndc(
+            f"addzone {zone} "
+            + "{ type primary; file "
+            + f'"{zone}.db"; dnssec-policy {policy}; '
+            + "};",
+        )
+        watcher.wait_for_line(
+            f"zone {zone}/IN (signed): zone_rekey failure: no SKR file"
+        )
+
+    shutil.copyfile(skr_fname, f"ns1/{skr_fname}")
+    with ns1.watch_log_from_here() as watcher:
+        ns1.rndc(f"skr -import {skr_fname} {zone}")
+        rekey = watcher.wait_for_line(
+            Re(rf"zone {zone}/IN \(signed\): zone_rekey (done|failure.*)")
+        )
+    assert rekey.group(1) == "done", f"zone_rekey {rekey.group(1)}"
+
+    isctest.kasp.check_dnssecstatus(ns1, zone, zsks, policy=policy, verbose=True)
+    isctest.kasp.check_dnssec_verify(ns1, zone)
+    check_keys(zsks, None, CONFIG, alg.dst, alg.bits, with_state=True)
+    isctest.kasp.check_apex(ns1, zone, ksks, zsks, offline_ksk=True)
+    isctest.kasp.check_subdomain(ns1, zone, ksks, zsks, offline_ksk=True)
+
+
 def test_ksr_oversize(ns1):
     zone = "invalid-skr.test"
     n = 1