2025-02-28 15:52:20 +01:00
|
|
|
# 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 os
|
|
|
|
|
|
|
|
from datetime import timedelta
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
pytest.importorskip("dns", minversion="2.0.0")
|
|
|
|
import dns.update
|
|
|
|
|
|
|
|
import isctest
|
2025-06-02 17:20:06 +02:00
|
|
|
from isctest.kasp import KeyTimingMetadata, Ipub, IpubC, Iret
|
2025-02-28 15:52:20 +01:00
|
|
|
|
2025-06-05 17:05:38 +02:00
|
|
|
from common import pytestmark
|
2025-02-28 15:52:20 +01:00
|
|
|
|
|
|
|
|
|
|
|
def test_rollover_manual(servers):
|
|
|
|
server = servers["ns3"]
|
|
|
|
policy = "manual-rollover"
|
|
|
|
config = {
|
|
|
|
"dnskey-ttl": timedelta(hours=1),
|
|
|
|
"ds-ttl": timedelta(days=1),
|
|
|
|
"max-zone-ttl": timedelta(days=1),
|
|
|
|
"parent-propagation-delay": timedelta(hours=1),
|
|
|
|
"publish-safety": timedelta(hours=1),
|
|
|
|
"retire-safety": timedelta(hours=1),
|
|
|
|
"signatures-refresh": timedelta(days=7),
|
|
|
|
"signatures-validity": timedelta(days=14),
|
|
|
|
"zone-propagation-delay": timedelta(minutes=5),
|
|
|
|
}
|
|
|
|
ttl = int(config["dnskey-ttl"].total_seconds())
|
|
|
|
alg = os.environ["DEFAULT_ALGORITHM_NUMBER"]
|
|
|
|
size = os.environ["DEFAULT_BITS"]
|
|
|
|
zone = "manual-rollover.kasp"
|
2025-03-19 10:10:13 +01:00
|
|
|
|
2025-06-17 14:32:49 +10:00
|
|
|
with server.watch_log_from_start() as watcher:
|
|
|
|
watcher.wait_for_line(f"keymgr: {zone} done")
|
|
|
|
|
2025-03-19 10:10:13 +01:00
|
|
|
isctest.kasp.check_dnssec_verify(server, zone)
|
|
|
|
|
2025-02-28 15:52:20 +01:00
|
|
|
key_properties = [
|
|
|
|
f"ksk unlimited {alg} {size} goal:omnipresent dnskey:omnipresent krrsig:omnipresent ds:omnipresent",
|
|
|
|
f"zsk unlimited {alg} {size} goal:omnipresent dnskey:omnipresent zrrsig:omnipresent",
|
|
|
|
]
|
|
|
|
expected = isctest.kasp.policy_to_properties(ttl, key_properties)
|
|
|
|
keys = isctest.kasp.keydir_to_keylist(zone, server.identifier)
|
|
|
|
ksks = [k for k in keys if k.is_ksk()]
|
|
|
|
zsks = [k for k in keys if not k.is_ksk()]
|
|
|
|
|
|
|
|
isctest.kasp.check_keys(zone, keys, expected)
|
|
|
|
|
|
|
|
offset = -timedelta(days=7)
|
|
|
|
for kp in expected:
|
|
|
|
kp.set_expected_keytimes(config, offset=offset)
|
|
|
|
|
|
|
|
isctest.kasp.check_keytimes(keys, expected)
|
|
|
|
isctest.kasp.check_dnssecstatus(server, zone, keys, policy=policy)
|
|
|
|
isctest.kasp.check_apex(server, zone, ksks, zsks)
|
|
|
|
isctest.kasp.check_subdomain(server, zone, ksks, zsks)
|
|
|
|
|
|
|
|
# Schedule KSK rollover in six months.
|
|
|
|
assert len(ksks) == 1
|
|
|
|
ksk = ksks[0]
|
|
|
|
startroll = expected[0].timing["Active"] + timedelta(days=30 * 6)
|
|
|
|
expected[0].timing["Retired"] = startroll + Ipub(config)
|
|
|
|
expected[0].timing["Removed"] = expected[0].timing["Retired"] + Iret(
|
|
|
|
config, zsk=False, ksk=True
|
|
|
|
)
|
|
|
|
|
|
|
|
with server.watch_log_from_here() as watcher:
|
|
|
|
server.rndc(f"dnssec -rollover -key {ksk.tag} -when {startroll} {zone}")
|
|
|
|
watcher.wait_for_line(f"keymgr: {zone} done")
|
|
|
|
|
2025-03-19 10:10:13 +01:00
|
|
|
isctest.kasp.check_dnssec_verify(server, zone)
|
2025-02-28 15:52:20 +01:00
|
|
|
isctest.kasp.check_keys(zone, keys, expected)
|
|
|
|
isctest.kasp.check_keytimes(keys, expected)
|
|
|
|
isctest.kasp.check_dnssecstatus(server, zone, keys, policy=policy)
|
|
|
|
isctest.kasp.check_apex(server, zone, ksks, zsks)
|
|
|
|
isctest.kasp.check_subdomain(server, zone, ksks, zsks)
|
|
|
|
|
|
|
|
# Schedule KSK rollover now.
|
|
|
|
now = KeyTimingMetadata.now()
|
|
|
|
with server.watch_log_from_here() as watcher:
|
|
|
|
server.rndc(f"dnssec -rollover -key {ksk.tag} -when {now} {zone}")
|
|
|
|
watcher.wait_for_line(f"keymgr: {zone} done")
|
|
|
|
|
2025-03-19 10:10:13 +01:00
|
|
|
isctest.kasp.check_dnssec_verify(server, zone)
|
|
|
|
|
2025-02-28 15:52:20 +01:00
|
|
|
key_properties = [
|
|
|
|
f"ksk unlimited {alg} {size} goal:hidden dnskey:omnipresent krrsig:omnipresent ds:omnipresent",
|
|
|
|
f"ksk unlimited {alg} {size} goal:omnipresent dnskey:rumoured krrsig:rumoured ds:hidden",
|
|
|
|
f"zsk unlimited {alg} {size} goal:omnipresent dnskey:omnipresent zrrsig:omnipresent",
|
|
|
|
]
|
|
|
|
expected = isctest.kasp.policy_to_properties(ttl, key_properties)
|
|
|
|
keys = isctest.kasp.keydir_to_keylist(zone, server.identifier)
|
|
|
|
ksks = [k for k in keys if k.is_ksk()]
|
|
|
|
zsks = [k for k in keys if not k.is_ksk()]
|
|
|
|
|
|
|
|
isctest.kasp.check_keys(zone, keys, expected)
|
|
|
|
|
|
|
|
expected[0].metadata["Successor"] = expected[1].key.tag
|
|
|
|
expected[1].metadata["Predecessor"] = expected[0].key.tag
|
|
|
|
isctest.kasp.check_keyrelationships(keys, expected)
|
|
|
|
|
|
|
|
for kp in expected:
|
|
|
|
off = offset
|
|
|
|
if "Predecessor" in kp.metadata:
|
|
|
|
off = 0
|
|
|
|
kp.set_expected_keytimes(config, offset=off)
|
|
|
|
|
|
|
|
expected[0].timing["Retired"] = now + Ipub(config)
|
|
|
|
expected[0].timing["Removed"] = expected[0].timing["Retired"] + Iret(
|
|
|
|
config, zsk=False, ksk=True
|
|
|
|
)
|
|
|
|
|
|
|
|
isctest.kasp.check_keytimes(keys, expected)
|
|
|
|
isctest.kasp.check_dnssecstatus(server, zone, keys, policy=policy)
|
|
|
|
isctest.kasp.check_apex(server, zone, ksks, zsks)
|
|
|
|
isctest.kasp.check_subdomain(server, zone, ksks, zsks)
|
|
|
|
|
|
|
|
# Schedule ZSK rollover now.
|
|
|
|
assert len(zsks) == 1
|
|
|
|
zsk = zsks[0]
|
|
|
|
now = KeyTimingMetadata.now()
|
|
|
|
with server.watch_log_from_here() as watcher:
|
|
|
|
server.rndc(f"dnssec -rollover -key {zsk.tag} -when {now} {zone}")
|
|
|
|
watcher.wait_for_line(f"keymgr: {zone} done")
|
|
|
|
|
2025-03-19 10:10:13 +01:00
|
|
|
isctest.kasp.check_dnssec_verify(server, zone)
|
|
|
|
|
2025-02-28 15:52:20 +01:00
|
|
|
key_properties = [
|
|
|
|
f"ksk unlimited {alg} {size} goal:hidden dnskey:omnipresent krrsig:omnipresent ds:omnipresent",
|
|
|
|
f"ksk unlimited {alg} {size} goal:omnipresent dnskey:rumoured krrsig:rumoured ds:hidden",
|
|
|
|
f"zsk unlimited {alg} {size} goal:hidden dnskey:omnipresent zrrsig:omnipresent",
|
|
|
|
f"zsk unlimited {alg} {size} goal:omnipresent dnskey:rumoured zrrsig:hidden",
|
|
|
|
]
|
|
|
|
expected = isctest.kasp.policy_to_properties(ttl, key_properties)
|
|
|
|
keys = isctest.kasp.keydir_to_keylist(zone, server.identifier)
|
|
|
|
ksks = [k for k in keys if k.is_ksk()]
|
|
|
|
zsks = [k for k in keys if not k.is_ksk()]
|
|
|
|
|
|
|
|
isctest.kasp.check_keys(zone, keys, expected)
|
|
|
|
|
|
|
|
expected[0].metadata["Successor"] = expected[1].key.tag
|
|
|
|
expected[1].metadata["Predecessor"] = expected[0].key.tag
|
|
|
|
expected[2].metadata["Successor"] = expected[3].key.tag
|
|
|
|
expected[3].metadata["Predecessor"] = expected[2].key.tag
|
|
|
|
isctest.kasp.check_keyrelationships(keys, expected)
|
|
|
|
|
|
|
|
# Try to schedule a ZSK rollover for an inactive key (should fail).
|
|
|
|
zsk = expected[3].key
|
|
|
|
response = server.rndc(f"dnssec -rollover -key {zsk.tag} {zone}")
|
|
|
|
assert "key is not actively signing" in response
|
2025-03-18 08:41:02 +01:00
|
|
|
|
|
|
|
|
|
|
|
def test_rollover_multisigner(servers):
|
|
|
|
server = servers["ns3"]
|
|
|
|
policy = "multisigner-model2"
|
|
|
|
config = {
|
|
|
|
"dnskey-ttl": timedelta(hours=1),
|
|
|
|
"ds-ttl": timedelta(days=1),
|
|
|
|
"max-zone-ttl": timedelta(days=1),
|
|
|
|
"parent-propagation-delay": timedelta(hours=1),
|
|
|
|
"publish-safety": timedelta(hours=1),
|
|
|
|
"retire-safety": timedelta(hours=1),
|
|
|
|
"signatures-refresh": timedelta(days=5),
|
|
|
|
"signatures-validity": timedelta(days=14),
|
|
|
|
"zone-propagation-delay": timedelta(minutes=5),
|
|
|
|
}
|
|
|
|
ttl = int(config["dnskey-ttl"].total_seconds())
|
|
|
|
alg = os.environ["DEFAULT_ALGORITHM_NUMBER"]
|
|
|
|
size = os.environ["DEFAULT_BITS"]
|
|
|
|
|
|
|
|
offset = -timedelta(days=7)
|
|
|
|
offval = int(offset.total_seconds())
|
|
|
|
|
|
|
|
def keygen(zone):
|
|
|
|
keygen_command = [
|
|
|
|
os.environ.get("KEYGEN"),
|
|
|
|
"-a",
|
|
|
|
alg,
|
|
|
|
"-L",
|
|
|
|
"3600",
|
|
|
|
"-M",
|
|
|
|
"0:32767",
|
|
|
|
zone,
|
|
|
|
]
|
|
|
|
|
2025-06-26 16:18:01 +02:00
|
|
|
return isctest.run.cmd(keygen_command).stdout.decode("utf-8")
|
2025-03-18 08:41:02 +01:00
|
|
|
|
|
|
|
zone = "multisigner-model2.kasp"
|
2025-03-19 10:10:13 +01:00
|
|
|
|
|
|
|
isctest.kasp.check_dnssec_verify(server, zone)
|
|
|
|
|
2025-03-18 08:41:02 +01:00
|
|
|
key_properties = [
|
|
|
|
f"ksk unlimited {alg} {size} goal:omnipresent dnskey:rumoured krrsig:rumoured ds:hidden tag-range:32768-65535",
|
|
|
|
f"zsk unlimited {alg} {size} goal:omnipresent dnskey:rumoured zrrsig:rumoured tag-range:32768-65535",
|
|
|
|
]
|
|
|
|
expected = isctest.kasp.policy_to_properties(ttl, key_properties)
|
|
|
|
|
|
|
|
newprops = [f"zsk unlimited {alg} {size} tag-range:0-32767"]
|
|
|
|
expected2 = isctest.kasp.policy_to_properties(ttl, newprops)
|
|
|
|
expected2[0].properties["private"] = False
|
|
|
|
expected2[0].properties["legacy"] = True
|
|
|
|
expected = expected + expected2
|
|
|
|
|
|
|
|
ownkeys = isctest.kasp.keydir_to_keylist(zone, server.identifier)
|
|
|
|
extkeys = isctest.kasp.keydir_to_keylist(zone)
|
|
|
|
keys = ownkeys + extkeys
|
|
|
|
ksks = [k for k in ownkeys if k.is_ksk()]
|
|
|
|
zsks = [k for k in ownkeys if not k.is_ksk()]
|
|
|
|
zsks = zsks + extkeys
|
|
|
|
|
|
|
|
isctest.kasp.check_keys(zone, keys, expected)
|
|
|
|
for kp in expected:
|
|
|
|
kp.set_expected_keytimes(config)
|
|
|
|
isctest.kasp.check_keytimes(keys, expected)
|
|
|
|
isctest.kasp.check_dnssecstatus(server, zone, keys, policy=policy)
|
|
|
|
isctest.kasp.check_apex(server, zone, ksks, zsks)
|
|
|
|
isctest.kasp.check_subdomain(server, zone, ksks, zsks)
|
|
|
|
|
|
|
|
# Update zone with ZSK from another provider for zone.
|
|
|
|
out = keygen(zone)
|
|
|
|
newkeys = isctest.kasp.keystr_to_keylist(out)
|
|
|
|
newprops = [f"zsk unlimited {alg} {size} tag-range:0-32767"]
|
|
|
|
expected2 = isctest.kasp.policy_to_properties(ttl, newprops)
|
|
|
|
expected2[0].properties["private"] = False
|
|
|
|
expected2[0].properties["legacy"] = True
|
|
|
|
expected = expected + expected2
|
|
|
|
|
|
|
|
dnskey = newkeys[0].dnskey().split()
|
|
|
|
rdata = " ".join(dnskey[4:])
|
|
|
|
|
2025-05-26 17:10:15 +02:00
|
|
|
update_msg = dns.update.UpdateMessage(zone)
|
|
|
|
update_msg.add(f"{dnskey[0]}", 3600, "DNSKEY", rdata)
|
|
|
|
server.nsupdate(update_msg)
|
2025-03-18 08:41:02 +01:00
|
|
|
|
2025-03-19 10:10:13 +01:00
|
|
|
isctest.kasp.check_dnssec_verify(server, zone)
|
|
|
|
|
2025-03-18 08:41:02 +01:00
|
|
|
keys = keys + newkeys
|
|
|
|
zsks = zsks + newkeys
|
|
|
|
isctest.kasp.check_keys(zone, keys, expected)
|
|
|
|
isctest.kasp.check_apex(server, zone, ksks, zsks)
|
|
|
|
isctest.kasp.check_subdomain(server, zone, ksks, zsks)
|
|
|
|
|
|
|
|
# Remove ZSKs from the other providers for zone.
|
|
|
|
dnskey2 = extkeys[0].dnskey().split()
|
|
|
|
rdata2 = " ".join(dnskey2[4:])
|
2025-05-26 17:10:15 +02:00
|
|
|
update_msg = dns.update.UpdateMessage(zone)
|
|
|
|
update_msg.delete(f"{dnskey[0]}", "DNSKEY", rdata)
|
|
|
|
update_msg.delete(f"{dnskey2[0]}", "DNSKEY", rdata2)
|
|
|
|
server.nsupdate(update_msg)
|
2025-03-18 08:41:02 +01:00
|
|
|
|
2025-03-19 10:10:13 +01:00
|
|
|
isctest.kasp.check_dnssec_verify(server, zone)
|
|
|
|
|
2025-03-18 08:41:02 +01:00
|
|
|
expected = isctest.kasp.policy_to_properties(ttl, key_properties)
|
|
|
|
keys = ownkeys
|
|
|
|
ksks = [k for k in ownkeys if k.is_ksk()]
|
|
|
|
zsks = [k for k in ownkeys if not k.is_ksk()]
|
|
|
|
isctest.kasp.check_keys(zone, keys, expected)
|
|
|
|
isctest.kasp.check_apex(server, zone, ksks, zsks)
|
|
|
|
isctest.kasp.check_subdomain(server, zone, ksks, zsks)
|
|
|
|
|
|
|
|
# A zone transitioning from single-signed to multi-signed. We should have
|
|
|
|
# the old omnipresent keys outside of the desired key range and the new
|
|
|
|
# keys in the desired key range.
|
|
|
|
zone = "single-to-multisigner.kasp"
|
2025-03-19 10:10:13 +01:00
|
|
|
|
|
|
|
isctest.kasp.check_dnssec_verify(server, zone)
|
|
|
|
|
2025-03-18 08:41:02 +01:00
|
|
|
key_properties = [
|
|
|
|
f"ksk unlimited {alg} {size} goal:omnipresent dnskey:rumoured krrsig:rumoured ds:hidden tag-range:32768-65535",
|
|
|
|
f"zsk unlimited {alg} {size} goal:omnipresent dnskey:rumoured zrrsig:hidden tag-range:32768-65535",
|
|
|
|
f"ksk unlimited {alg} {size} goal:hidden dnskey:omnipresent krrsig:omnipresent ds:omnipresent tag-range:0-32767 offset:{offval}",
|
|
|
|
f"zsk unlimited {alg} {size} goal:hidden dnskey:omnipresent zrrsig:omnipresent tag-range:0-32767 offset:{offval}",
|
|
|
|
]
|
|
|
|
expected = isctest.kasp.policy_to_properties(ttl, key_properties)
|
|
|
|
keys = isctest.kasp.keydir_to_keylist(zone, server.identifier)
|
|
|
|
ksks = [k for k in keys if k.is_ksk()]
|
|
|
|
zsks = [k for k in keys if not k.is_ksk()]
|
|
|
|
|
|
|
|
isctest.kasp.check_keys(zone, keys, expected)
|
|
|
|
|
|
|
|
for kp in expected:
|
|
|
|
kp.set_expected_keytimes(config)
|
|
|
|
|
|
|
|
start = expected[0].key.get_timing("Created")
|
|
|
|
expected[2].timing["Retired"] = start
|
|
|
|
expected[2].timing["Removed"] = expected[2].timing["Retired"] + Iret(
|
|
|
|
config, zsk=False, ksk=True
|
|
|
|
)
|
|
|
|
expected[3].timing["Retired"] = start
|
|
|
|
expected[3].timing["Removed"] = expected[3].timing["Retired"] + Iret(
|
|
|
|
config, zsk=True, ksk=False
|
|
|
|
)
|
|
|
|
|
|
|
|
isctest.kasp.check_keytimes(keys, expected)
|
|
|
|
isctest.kasp.check_dnssecstatus(server, zone, keys, policy=policy)
|
|
|
|
isctest.kasp.check_apex(server, zone, ksks, zsks)
|
|
|
|
isctest.kasp.check_subdomain(server, zone, ksks, zsks)
|