# 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 isctest import isctest.mark pytestmark = pytest.mark.extra_artifacts( [ "*.axfr", "*.created", "created.key-*", "dig.out*", "ns*/*.mkeys*", "ns*/dsset-*", "ns*/K*.key", "ns*/K*.private", "ns*/K*.state", "ns*/kasp.conf", "ns*/keygen.out*", "ns*/managed-keys.bind*", "ns*/named.conf", "ns*/named.memstats", "ns*/named.run", "ns*/signer.out*", "ns*/zones", "ns*/*.db", "ns*/*.db.infile", "ns*/*.db.jbk", "ns*/*.db.jnl", "ns*/*.db.signed*", "python.out.*", "retired.*", "rndc.dnssec.*", "unused.key*", "verify.out.*", ] ) default_config = { "dnskey-ttl": timedelta(hours=1), "ds-ttl": timedelta(days=1), "key-directory": "{keydir}", "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), } standard_config = { "dnskey-ttl": timedelta(seconds=7200), "ds-ttl": timedelta(days=1), "key-directory": "{keydir}", "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), } timing_config = { "dnskey-ttl": timedelta(seconds=300), "ds-ttl": timedelta(seconds=7200), "key-directory": "{keydir}", "max-zone-ttl": timedelta(hours=11), "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(seconds=3600), } migrate_config = { "dnskey-ttl": timedelta(seconds=300), "ds-ttl": timedelta(seconds=7200), "key-directory": "{keydir}", "max-zone-ttl": timedelta(hours=11), "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(seconds=3600), } view_config = { "dnskey-ttl": timedelta(seconds=300), "ds-ttl": timedelta(seconds=86400), "key-directory": "{keydir}", "max-zone-ttl": timedelta(days=1), "parent-propagation-delay": timedelta(hours=3), "publish-safety": timedelta(hours=1), "retire-safety": timedelta(hours=1), "signatures-refresh": timedelta(days=5), "signatures-validity": timedelta(days=14), "zone-propagation-delay": timedelta(seconds=300), } lifetime = { "P60D": int(timedelta(days=60).total_seconds()), "P3M": int(timedelta(days=31 * 3).total_seconds()), "P1Y": int(timedelta(days=365).total_seconds()), } @pytest.mark.parametrize( "params", [ # Testing good migration (KSK/ZSK). pytest.param( { "zone": "migrate.kasp", "policy": "migrate", "server": "ns3", "config": standard_config, "offset": 0, "key-properties": [ f"ksk 0 {os.environ['DEFAULT_ALGORITHM_NUMBER']} {os.environ['DEFAULT_BITS']} goal:omnipresent dnskey:rumoured krrsig:rumoured ds:rumoured", f"zsk {lifetime['P60D']} {os.environ['DEFAULT_ALGORITHM_NUMBER']} {os.environ['DEFAULT_BITS']} goal:omnipresent dnskey:rumoured zrrsig:rumoured", ], }, id="migrate.kasp", ), # Testing a good migration (CSK). pytest.param( { "zone": "csk.kasp", "policy": "default", "server": "ns3", "config": default_config, "offset": 0, "key-properties": [ f"csk 0 {os.environ['DEFAULT_ALGORITHM_NUMBER']} {os.environ['DEFAULT_BITS']} goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:rumoured", ], }, id="csk.kasp", ), # Testing a good migration (CSK, no SEP). pytest.param( { "zone": "csk-nosep.kasp", "policy": "default", "server": "ns3", "config": default_config, "offset": 0, "key-properties": [ f"csk 0 {os.environ['DEFAULT_ALGORITHM_NUMBER']} {os.environ['DEFAULT_BITS']} goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:rumoured", ], }, id="csk-nosep.kasp", ), # Testing key states derived from timing metadata: rumoured. pytest.param( { "zone": "rumoured.kasp", "policy": "timing-metadata", "server": "ns3", "config": timing_config, "offset": -timedelta(seconds=300), "key-properties": [ f"ksk {lifetime['P60D']} {os.environ['DEFAULT_ALGORITHM_NUMBER']} {os.environ['DEFAULT_BITS']} goal:omnipresent dnskey:rumoured krrsig:rumoured ds:rumoured", f"zsk {lifetime['P60D']} {os.environ['DEFAULT_ALGORITHM_NUMBER']} {os.environ['DEFAULT_BITS']} goal:omnipresent dnskey:rumoured zrrsig:rumoured", ], }, id="rumoured.kasp", ), # Testing key states derived from timing metadata: omnipresent. pytest.param( { "zone": "omnipresent.kasp", "policy": "timing-metadata", "server": "ns3", "config": timing_config, "offset": -timedelta(seconds=3900), "key-properties": [ f"ksk {lifetime['P60D']} {os.environ['DEFAULT_ALGORITHM_NUMBER']} {os.environ['DEFAULT_BITS']} goal:omnipresent dnskey:omnipresent krrsig:omnipresent ds:omnipresent", f"zsk {lifetime['P60D']} {os.environ['DEFAULT_ALGORITHM_NUMBER']} {os.environ['DEFAULT_BITS']} goal:omnipresent dnskey:omnipresent zrrsig:omnipresent", ], }, id="omnipresent.kasp", ), # Testing key states derived from timing metadata: no SyncPublish. pytest.param( { "zone": "no-syncpublish.kasp", "policy": "timing-metadata", "server": "ns3", "config": timing_config, "offset": -timedelta(hours=12), "key-properties": [ f"ksk {lifetime['P60D']} {os.environ['DEFAULT_ALGORITHM_NUMBER']} {os.environ['DEFAULT_BITS']} goal:omnipresent dnskey:omnipresent krrsig:omnipresent ds:rumoured", f"zsk {lifetime['P60D']} {os.environ['DEFAULT_ALGORITHM_NUMBER']} {os.environ['DEFAULT_BITS']} goal:omnipresent dnskey:omnipresent zrrsig:omnipresent", ], }, id="no-syncpublish.kasp", ), # Test migration to dnssec-policy, existing keys do not match key algorithm. pytest.param( { "zone": "migrate-nomatch-algnum.kasp", "policy": "migrate-nomatch-algnum", "server": "ns3", "config": migrate_config, "offset": -timedelta(seconds=3900), "key-properties": [ "ksk - 8 2048 goal:hidden dnskey:omnipresent krrsig:omnipresent ds:omnipresent", "zsk - 8 2048 goal:hidden dnskey:omnipresent zrrsig:omnipresent", f"ksk 0 {os.environ['DEFAULT_ALGORITHM_NUMBER']} {os.environ['DEFAULT_BITS']} goal:omnipresent dnskey:rumoured krrsig:rumoured ds:hidden", f"zsk {lifetime['P60D']} {os.environ['DEFAULT_ALGORITHM_NUMBER']} {os.environ['DEFAULT_BITS']} goal:omnipresent dnskey:rumoured zrrsig:rumoured", ], }, id="migrate-nomatch-algnum.kasp", ), # Test migration to dnssec-policy, existing keys do not match key length. pytest.param( { "zone": "migrate-nomatch-alglen.kasp", "policy": "migrate-nomatch-alglen", "server": "ns3", "config": migrate_config, "offset": -timedelta(seconds=3900), "key-properties": [ "ksk - 8 2048 goal:hidden dnskey:omnipresent krrsig:omnipresent ds:omnipresent", "zsk - 8 2048 goal:hidden dnskey:omnipresent zrrsig:omnipresent", "ksk 0 8 3072 goal:omnipresent dnskey:rumoured krrsig:rumoured ds:hidden", # This key is considered to be prepublished, so it is not yet signing. f"zsk {lifetime['P60D']} 8 3072 goal:omnipresent dnskey:rumoured zrrsig:hidden", ], }, id="migrate-nomatch-alglen.kasp", ), # Test migration to dnssec-policy, existing keys do not match role (KSK/ZSK -> CSK). pytest.param( { "zone": "migrate-nomatch-kzc.kasp", "policy": "migrate-nomatch-kzc", "server": "ns3", "config": migrate_config, "offset": -timedelta(seconds=3900), "key-properties": [ f"ksk - {os.environ['DEFAULT_ALGORITHM_NUMBER']} {os.environ['DEFAULT_BITS']} goal:hidden dnskey:omnipresent krrsig:omnipresent ds:omnipresent", f"zsk - {os.environ['DEFAULT_ALGORITHM_NUMBER']} {os.environ['DEFAULT_BITS']} goal:hidden dnskey:omnipresent zrrsig:omnipresent", # This key is considered to be prepublished, so it is not yet signing, nor is the DS introduced. f"csk 0 {os.environ['DEFAULT_ALGORITHM_NUMBER']} {os.environ['DEFAULT_BITS']} goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:hidden ds:hidden", ], }, id="migrate-nomatch-kzc.kasp", ), # Test good migration with views. pytest.param( { "zone": "view-rsasha256.kasp", "policy": "rsasha256", "server": "ns4", "config": view_config, "offset": -timedelta(days=31 * 3), "key-properties": [ f"zsk {lifetime['P3M']} 8 2048 goal:hidden dnskey:omnipresent zrrsig:omnipresent", f"zsk {lifetime['P3M']} 8 2048 goal:omnipresent dnskey:rumoured zrrsig:hidden", f"ksk {lifetime['P1Y']} 8 2048 goal:omnipresent dnskey:omnipresent krrsig:omnipresent ds:omnipresent", ], "view": "ext", "tsig": "external:YPfMoAk6h+3iN8MDRQC004iSNHY=", }, id="view-rsasha256.kasp (external)", ), pytest.param( { "zone": "view-rsasha256.kasp", "policy": "rsasha256", "server": "ns4", "config": view_config, "offset": -timedelta(days=31 * 3), "key-properties": [ f"zsk {lifetime['P3M']} 8 2048 goal:hidden dnskey:omnipresent zrrsig:omnipresent", f"zsk {lifetime['P3M']} 8 2048 goal:omnipresent dnskey:rumoured zrrsig:hidden", f"ksk {lifetime['P1Y']} 8 2048 goal:omnipresent dnskey:omnipresent krrsig:omnipresent ds:omnipresent", ], "view": "int", "tsig": "internal:4xILSZQnuO1UKubXHkYUsvBRPu8=", }, id="view-rsasha256.kasp (internal)", ), ], ) def test_migrate2kasp_case(servers, params): # Get test parameters. zone = params["zone"] policy = params["policy"] server = servers[params["server"]] keydir = server.identifier view = params.get("view", None) tsig = None if "tsig" in params: secret = params["tsig"] tsig = f"{os.environ['DEFAULT_HMAC']}:{secret}" isctest.kasp.wait_keymgr_done(server, zone) params["config"]["key-directory"] = params["config"]["key-directory"].replace( "{keydir}", keydir ) ttl = int(params["config"]["dnskey-ttl"].total_seconds()) # Test case. isctest.log.info(f"check test case zone {zone} policy {policy}") # First make sure the zone is signed. isctest.kasp.check_dnssec_verify(server, zone, tsig=tsig) # Key properties. expected = isctest.kasp.policy_to_properties(ttl=ttl, keys=params["key-properties"]) # Special case: CSK without SEP bit set. if zone == "csk-nosep.kasp": expected[0].properties["role_full"] = "zone-signing" expected[0].properties["flags"] = 256 # Key files. keys = isctest.kasp.keydir_to_keylist(zone, params["config"]["key-directory"]) 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 = params["offset"] if "offset" in params else None for expect in expected: expect.set_expected_keytimes(params["config"], offset=offset, migrate=True) isctest.kasp.check_dnssecstatus(server, zone, ksks + zsks, policy=policy, view=view) isctest.kasp.check_apex(server, zone, ksks, zsks, tsig=tsig) isctest.kasp.check_subdomain(server, zone, ksks, zsks, tsig=tsig) if "additional-tests" in params: for additional_test in params["additional-tests"]: callback = additional_test["callback"] arguments = additional_test["arguments"] callback(*arguments, params=params, ksks=ksks, zsks=zsks)