Add fast test case

This specific test case triggered a bug where the SKR included bundles
with unsigned DNSKEY RRsets (signatures where omitted because the
inception time was equal to the inactive time of the key).
This commit is contained in:
Matthijs Mekking 2026-04-02 10:06:08 +02:00
parent bc6dad585d
commit 784a441e2d
3 changed files with 204 additions and 41 deletions

View file

@ -81,6 +81,25 @@ dnssec-policy "ksk-roll" {
};
};
dnssec-policy "fast" {
offline-ksk yes;
keys {
ksk lifetime unlimited algorithm @DEFAULT_ALGORITHM@;
zsk lifetime 8792 algorithm @DEFAULT_ALGORITHM@;
};
dnskey-ttl 439;
max-zone-ttl 4396;
zone-propagation-delay 439;
signatures-validity 6;
signatures-validity-dnskey 6;
signatures-refresh 2;
signatures-jitter 0;
publish-safety 1;
retire-safety 1;
parent-ds-ttl 5;
parent-propagation-delay 5;
};
dnssec-policy "invalid-skr" {
offline-ksk yes;
keys {

View file

@ -28,3 +28,4 @@ cp template.db.in unlimited.test.db
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

View file

@ -20,6 +20,7 @@ import pytest
from isctest.kasp import KeyTimingMetadata
from isctest.vars.algorithms import Algorithm
from rollover.common import TIMEDELTA
import isctest
@ -27,6 +28,7 @@ pytestmark = pytest.mark.extra_artifacts(
[
"K*",
"common.test.*",
"fast.test.*",
"future.test.*",
"in-the-middle.test.*",
"ksk-roll.test.*",
@ -43,6 +45,11 @@ pytestmark = pytest.mark.extra_artifacts(
"ns1/common.test.db.signed",
"ns1/common.test.db.signed.jnl",
"ns1/common.test.skr.2",
"ns1/fast.test.db",
"ns1/fast.test.db.jbk",
"ns1/fast.test.db.signed",
"ns1/fast.test.db.signed.jnl",
"ns1/fast.test.skr.1",
"ns1/future.test.db",
"ns1/future.test.db.jbk",
"ns1/future.test.db.signed",
@ -86,6 +93,30 @@ pytestmark = pytest.mark.extra_artifacts(
]
)
CONFIG = {
"dnskey-ttl": TIMEDELTA["PT1H"],
"ds-ttl": TIMEDELTA["P1D"],
"max-zone-ttl": TIMEDELTA["P1D"],
"parent-propagation-delay": TIMEDELTA["PT1H"],
"publish-safety": TIMEDELTA["PT1H"],
"retire-safety": TIMEDELTA["PT1H"],
"signatures-refresh": TIMEDELTA["P5D"],
"signatures-validity": TIMEDELTA["P14D"],
"zone-propagation-delay": TIMEDELTA["PT5M"],
}
FASTCONFIG = {
"dnskey-ttl": timedelta(seconds=439),
"ds-ttl": TIMEDELTA["PT1H"],
"max-zone-ttl": timedelta(seconds=4396),
"parent-propagation-delay": timedelta(seconds=5),
"publish-safety": timedelta(seconds=1),
"retire-safety": timedelta(seconds=1),
"signatures-refresh": timedelta(seconds=2),
"signatures-validity": timedelta(seconds=6),
"zone-propagation-delay": timedelta(seconds=439),
}
def between(value, start, end):
if value is None or start is None or end is None:
@ -116,9 +147,14 @@ def ksr(zone, policy, action, options="", raise_on_exception=True, to_file=""):
return cmd
def sign_delay(config):
return config["signatures-validity"] - config["signatures-refresh"]
def check_keys(
keys,
lifetime,
config,
alg=None,
size=None,
offset=0,
@ -144,7 +180,12 @@ def check_keys(
active = retired
# published: dnskey-ttl + publish-safety + propagation
published = active - timedelta(hours=2, minutes=5)
pubtime = (
config["dnskey-ttl"]
+ config["publish-safety"]
+ config["zone-propagation-delay"]
)
published = active - pubtime
# retired: zsk-lifetime
if lifetime is not None:
@ -152,10 +193,22 @@ def check_keys(
if key.is_ksk():
# removed: ttlds + retire-safety + parent-propagation
removed = retired + timedelta(days=1, hours=2)
remtime = (
config["ds-ttl"]
+ config["retire-safety"]
+ config["parent-propagation-delay"]
)
removed = retired + remtime
else:
# removed: ttlsig + retire-safety + sign-delay + propagation
removed = retired + timedelta(days=10, hours=1, minutes=5)
remtime = (
config["max-zone-ttl"]
+ config["retire-safety"]
+ config["zone-propagation-delay"]
+ sign_delay(config)
)
removed = retired + remtime
else:
retired = None
removed = None
@ -167,8 +220,8 @@ def check_keys(
state_ds = "hidden"
if retired is None or between(now, published, retired):
goal = "omnipresent"
pubdelay = published + timedelta(hours=2, minutes=5)
signdelay = active + timedelta(days=10, hours=1, minutes=5)
pubdelay = published + pubtime
signdelay = active + sign_delay(config)
if between(now, published, pubdelay):
state_dnskey = "rumoured"
@ -261,19 +314,19 @@ def check_cds_bundle(bundle_keys, bundle_lines, expected_cds):
assert count == len(bundle_lines)
def check_rrsig_bundle(bundle_keys, bundle_lines, zone, rrtype, sigend, sigstart):
def check_rrsig_bundle(bundle_keys, bundle_lines, zone, rrtype, sigend, sigstart, ttl):
count = 0
for key in bundle_keys:
found = False
alg = key.get_dnsalg()
expect = f"{zone}. 3600 IN RRSIG {rrtype} {alg} 2 3600 {sigend} {sigstart} {key.tag} {zone}."
expect = f"{zone}. {ttl} IN RRSIG {rrtype} {alg} 2 {ttl} {sigend} {sigstart} {key.tag} {zone}."
# there must be a signature of this ksk
for line in bundle_lines:
rrsig = " ".join(line.split())
if expect in rrsig:
found = True
count += 1
assert found
assert found, f"Expected string not found: {expect}"
assert count == len(bundle_keys)
assert count == len(bundle_lines)
@ -331,6 +384,7 @@ def check_keysigningrequest(path, zsks, start, end):
def check_signedkeyresponse(
path,
config,
zone,
ksks,
zsks,
@ -345,6 +399,8 @@ def check_signedkeyresponse(
line_no = 0
next_bundle = end + 1
dnskey_ttl = int(config["dnskey-ttl"].total_seconds())
inception = start
while inception < end:
# A single signed key response may consist of:
@ -358,7 +414,7 @@ def check_signedkeyresponse(
# ;; RRSIG(CDS) (one per active key in ksks)
sigstart = inception - timedelta(hours=1) # clockskew
sigend = inception + timedelta(days=14) # sig-validity
sigend = inception + config["signatures-validity"]
next_bundle = sigend + refresh
# ignore empty lines
@ -440,7 +496,9 @@ def check_signedkeyresponse(
bundle_lines.append(lines[line_no])
line_no += 1
check_rrsig_bundle(bundle_keys, bundle_lines, zone, "DNSKEY", sigend, sigstart)
check_rrsig_bundle(
bundle_keys, bundle_lines, zone, "DNSKEY", sigend, sigstart, dnskey_ttl
)
# expect cdnskey
have_cdnskey = False
@ -489,7 +547,13 @@ def check_signedkeyresponse(
line_no += 1
check_rrsig_bundle(
bundle_keys, bundle_lines, zone, "CDNSKEY", sigend, sigstart
bundle_keys,
bundle_lines,
zone,
"CDNSKEY",
sigend,
sigstart,
dnskey_ttl,
)
# expect cds
@ -540,7 +604,9 @@ def check_signedkeyresponse(
bundle_lines.append(lines[line_no])
line_no += 1
check_rrsig_bundle(bundle_keys, bundle_lines, zone, "CDS", sigend, sigstart)
check_rrsig_bundle(
bundle_keys, bundle_lines, zone, "CDS", sigend, sigstart, dnskey_ttl
)
inception = next_bundle
@ -598,7 +664,7 @@ def test_ksr_common(ns1):
ksks = isctest.kasp.keystr_to_keylist(cmd.out, kskdir)
assert len(ksks) == 1
check_keys(ksks, None)
check_keys(ksks, None, CONFIG)
# check that 'dnssec-ksr keygen' pregenerates right amount of keys
cmd = ksr(zone, policy, "keygen", options="-i now -e +1y")
@ -606,7 +672,7 @@ def test_ksr_common(ns1):
assert len(zsks) == 2
lifetime = timedelta(days=31 * 6)
check_keys(zsks, lifetime)
check_keys(zsks, lifetime, CONFIG)
# check that 'dnssec-ksr keygen' pregenerates right amount of keys
# in the given key directory
@ -616,7 +682,7 @@ def test_ksr_common(ns1):
assert len(zsks) == 2
lifetime = timedelta(days=31 * 6)
check_keys(zsks, lifetime)
check_keys(zsks, lifetime, CONFIG)
for key in zsks:
privatefile = f"{key.path}.private"
@ -649,7 +715,7 @@ def test_ksr_common(ns1):
options=f"-K {kskdir} -f {ksr_fname} -i {now} -e +1y",
to_file=skr_fname,
)
check_signedkeyresponse(skr_fname, zone, ksks, zsks, now, until, refresh)
check_signedkeyresponse(skr_fname, CONFIG, zone, ksks, zsks, now, until, refresh)
# common test cases (2)
n = 2
@ -699,7 +765,7 @@ def test_ksr_common(ns1):
cmd = ksr(zone, policy, "keygen", options=f"-K {zskdir} -i {now} -e +2y")
overlapping_zsks2 = isctest.kasp.keystr_to_keylist(cmd.out, zskdir)
assert len(overlapping_zsks2) == 4
check_keys(overlapping_zsks2, lifetime)
check_keys(overlapping_zsks2, lifetime, CONFIG)
for index, key in enumerate(overlapping_zsks2):
assert overlapping_zsks[index] == key
@ -740,6 +806,7 @@ def test_ksr_common(ns1):
)
check_signedkeyresponse(
skr_fname,
CONFIG,
zone,
ksks,
overlapping_zsks,
@ -768,7 +835,7 @@ def test_ksr_common(ns1):
# - dnssec_verify
isctest.kasp.check_dnssec_verify(ns1, zone)
# - check keys
check_keys(overlapping_zsks, lifetime, with_state=True)
check_keys(overlapping_zsks, lifetime, CONFIG, with_state=True)
# - check apex
isctest.kasp.check_apex(ns1, zone, ksks, overlapping_zsks, offline_ksk=True)
# - check subdomain
@ -787,7 +854,7 @@ def test_ksr_lastbundle(ns1):
ksks = isctest.kasp.keystr_to_keylist(cmd.out, kskdir)
assert len(ksks) == 1
check_keys(ksks, None, offset=offset)
check_keys(ksks, None, CONFIG, offset=offset)
# check that 'dnssec-ksr keygen' pregenerates right amount of keys
zskdir = "ns1"
@ -796,7 +863,7 @@ def test_ksr_lastbundle(ns1):
assert len(zsks) == 2
lifetime = timedelta(days=31 * 6)
check_keys(zsks, lifetime, offset=offset)
check_keys(zsks, lifetime, CONFIG, offset=offset)
# check that 'dnssec-ksr request' creates correct ksr
then = zsks[0].get_timing("Created") + offset
@ -821,7 +888,7 @@ def test_ksr_lastbundle(ns1):
options=f"-K {kskdir} -f {ksr_fname} -i {then} -e +1d",
to_file=skr_fname,
)
check_signedkeyresponse(skr_fname, zone, ksks, zsks, then, until, refresh)
check_signedkeyresponse(skr_fname, CONFIG, zone, ksks, zsks, then, until, refresh)
# add zone
ns1.rndc(
@ -841,7 +908,7 @@ def test_ksr_lastbundle(ns1):
# - dnssec_verify
isctest.kasp.check_dnssec_verify(ns1, zone)
# - check keys
check_keys(zsks, lifetime, offset=offset, with_state=True)
check_keys(zsks, lifetime, CONFIG, offset=offset, with_state=True)
# - check apex
isctest.kasp.check_apex(ns1, zone, ksks, zsks, offline_ksk=True)
# - check subdomain
@ -864,7 +931,7 @@ def test_ksr_inthemiddle(ns1):
ksks = isctest.kasp.keystr_to_keylist(cmd.out, kskdir)
assert len(ksks) == 1
check_keys(ksks, None, offset=offset)
check_keys(ksks, None, CONFIG, offset=offset)
# check that 'dnssec-ksr keygen' pregenerates right amount of keys
zskdir = "ns1"
@ -873,7 +940,7 @@ def test_ksr_inthemiddle(ns1):
assert len(zsks) == 4
lifetime = timedelta(days=31 * 6)
check_keys(zsks, lifetime, offset=offset)
check_keys(zsks, lifetime, CONFIG, offset=offset)
# check that 'dnssec-ksr request' creates correct ksr
then = zsks[0].get_timing("Created")
@ -899,7 +966,7 @@ def test_ksr_inthemiddle(ns1):
options=f"-K {kskdir} -f {ksr_fname} -i {then} -e +1y",
to_file=skr_fname,
)
check_signedkeyresponse(skr_fname, zone, ksks, zsks, then, until, refresh)
check_signedkeyresponse(skr_fname, CONFIG, zone, ksks, zsks, then, until, refresh)
# add zone
ns1.rndc(
@ -919,7 +986,7 @@ def test_ksr_inthemiddle(ns1):
# - dnssec_verify
isctest.kasp.check_dnssec_verify(ns1, zone)
# - check keys
check_keys(zsks, lifetime, offset=offset, with_state=True)
check_keys(zsks, lifetime, CONFIG, offset=offset, with_state=True)
# - check apex
isctest.kasp.check_apex(ns1, zone, ksks, zsks, offline_ksk=True)
# - check subdomain
@ -1014,7 +1081,7 @@ def test_ksr_unlimited(ns1):
ksks = isctest.kasp.keystr_to_keylist(cmd.out, kskdir)
assert len(ksks) == 1
check_keys(ksks, None)
check_keys(ksks, None, CONFIG)
# check that 'dnssec-ksr keygen' pregenerates right amount of keys
zskdir = "ns1"
@ -1023,7 +1090,7 @@ def test_ksr_unlimited(ns1):
assert len(zsks) == 1
lifetime = None
check_keys(zsks, lifetime)
check_keys(zsks, lifetime, CONFIG)
# check that 'dnssec-ksr request' creates correct ksr
now = zsks[0].get_timing("Created")
@ -1050,6 +1117,7 @@ def test_ksr_unlimited(ns1):
)
check_signedkeyresponse(
skr_fname,
CONFIG,
zone,
ksks,
zsks,
@ -1072,6 +1140,7 @@ def test_ksr_unlimited(ns1):
)
check_signedkeyresponse(
skr_fname,
CONFIG,
zone,
ksks,
zsks,
@ -1091,7 +1160,7 @@ def test_ksr_unlimited(ns1):
options=f"-K {kskdir} -f {ksr_fname} -i {now} -e +4y",
to_file=skr_fname,
)
check_signedkeyresponse(skr_fname, zone, ksks, zsks, now, until, refresh)
check_signedkeyresponse(skr_fname, CONFIG, zone, ksks, zsks, now, until, refresh)
# add zone
ns1.rndc(
@ -1111,7 +1180,7 @@ def test_ksr_unlimited(ns1):
# - dnssec_verify
isctest.kasp.check_dnssec_verify(ns1, zone)
# - check keys
check_keys(zsks, lifetime, with_state=True)
check_keys(zsks, lifetime, CONFIG, with_state=True)
# - check apex
isctest.kasp.check_apex(ns1, zone, ksks, zsks, offline_ksk=True)
# - check subdomain
@ -1141,11 +1210,11 @@ def test_ksr_twotone(ns1):
assert len(ksks_defalg) == 1
assert len(ksks_altalg) == 1
check_keys(ksks_defalg, None)
check_keys(ksks_defalg, None, CONFIG)
alg = os.environ.get("ALTERNATIVE_ALGORITHM_DST_NUMBER")
size = os.environ.get("ALTERNATIVE_BITS")
check_keys(ksks_altalg, None, alg, size)
check_keys(ksks_altalg, None, CONFIG, alg, size)
# check that 'dnssec-ksr keygen' pregenerates right amount of keys
zskdir = "ns1"
@ -1171,12 +1240,12 @@ def test_ksr_twotone(ns1):
assert len(zsks_altalg) == 3
lifetime = timedelta(days=31 * 3)
check_keys(zsks_defalg, lifetime)
check_keys(zsks_defalg, lifetime, CONFIG)
alg = os.environ.get("ALTERNATIVE_ALGORITHM_DST_NUMBER")
size = os.environ.get("ALTERNATIVE_BITS")
lifetime = timedelta(days=31 * 5)
check_keys(zsks_altalg, lifetime, alg, size)
check_keys(zsks_altalg, lifetime, CONFIG, alg, size)
# check that 'dnssec-ksr request' creates correct ksr
now = zsks[0].get_timing("Created")
@ -1201,7 +1270,7 @@ def test_ksr_twotone(ns1):
options=f"-K {kskdir} -f {ksr_fname} -i {now} -e +1y",
to_file=skr_fname,
)
check_signedkeyresponse(skr_fname, zone, ksks, zsks, now, until, refresh)
check_signedkeyresponse(skr_fname, CONFIG, zone, ksks, zsks, now, until, refresh)
# add zone
ns1.rndc(
@ -1222,12 +1291,12 @@ def test_ksr_twotone(ns1):
isctest.kasp.check_dnssec_verify(ns1, zone)
# - check keys
lifetime = timedelta(days=31 * 3)
check_keys(zsks_defalg, lifetime, with_state=True)
check_keys(zsks_defalg, lifetime, CONFIG, with_state=True)
alg = os.environ.get("ALTERNATIVE_ALGORITHM_DST_NUMBER")
size = os.environ.get("ALTERNATIVE_BITS")
lifetime = timedelta(days=31 * 5)
check_keys(zsks_altalg, lifetime, alg, size, with_state=True)
check_keys(zsks_altalg, lifetime, CONFIG, alg, size, with_state=True)
# - check apex
isctest.kasp.check_apex(ns1, zone, ksks, zsks, offline_ksk=True)
# - check subdomain
@ -1246,7 +1315,7 @@ def test_ksr_kskroll(ns1):
assert len(ksks) == 2
lifetime = timedelta(days=31 * 6)
check_keys(ksks, lifetime)
check_keys(ksks, lifetime, CONFIG)
# check that 'dnssec-ksr keygen' pregenerates right amount of keys
zskdir = "ns1"
@ -1254,7 +1323,7 @@ def test_ksr_kskroll(ns1):
zsks = isctest.kasp.keystr_to_keylist(cmd.out, zskdir)
assert len(zsks) == 1
check_keys(zsks, None)
check_keys(zsks, None, CONFIG)
# check that 'dnssec-ksr request' creates correct ksr
now = zsks[0].get_timing("Created")
@ -1279,7 +1348,7 @@ def test_ksr_kskroll(ns1):
options=f"-K {kskdir} -f {ksr_fname} -i {now} -e +1y",
to_file=skr_fname,
)
check_signedkeyresponse(skr_fname, zone, ksks, zsks, now, until, refresh)
check_signedkeyresponse(skr_fname, CONFIG, zone, ksks, zsks, now, until, refresh)
# add zone
ns1.rndc(
@ -1299,7 +1368,81 @@ def test_ksr_kskroll(ns1):
# - dnssec_verify
isctest.kasp.check_dnssec_verify(ns1, zone)
# - check keys
check_keys(zsks, None, with_state=True)
check_keys(zsks, None, CONFIG, with_state=True)
# - check apex
isctest.kasp.check_apex(ns1, zone, ksks, zsks, offline_ksk=True)
# - check subdomain
isctest.kasp.check_subdomain(ns1, zone, ksks, zsks, offline_ksk=True)
def test_ksr_fast(ns1):
zone = "fast.test"
policy = "fast"
n = 1
# create ksk
kskdir = "ns1/offline"
cmd = ksr(zone, policy, "keygen", options=f"-K {kskdir} -i now -e +1h -o")
ksks = isctest.kasp.keystr_to_keylist(cmd.out, kskdir)
assert len(ksks) == 1
check_keys(ksks, None, FASTCONFIG)
# check that 'dnssec-ksr keygen' pregenerates right amount of keys
zskdir = "ns1"
cmd = ksr(zone, policy, "keygen", options=f"-K {zskdir} -i now -e +1h")
zsks = isctest.kasp.keystr_to_keylist(cmd.out, zskdir)
assert len(zsks) == 1
lifetime = timedelta(seconds=8792)
check_keys(zsks, lifetime, FASTCONFIG)
# check that 'dnssec-ksr request' creates correct ksr
now = zsks[0].get_timing("Created")
until = now + timedelta(hours=1)
ksr_fname = f"{zone}.ksr.{n}"
ksr(
zone,
policy,
"request",
options=f"-K {zskdir} -i {now} -e +1h",
to_file=ksr_fname,
)
check_keysigningrequest(ksr_fname, zsks, now, until)
# check that 'dnssec-ksr sign' creates correct skr
refresh = -2
skr_fname = f"{zone}.skr.{n}"
ksr(
zone,
policy,
"sign",
options=f"-K {kskdir} -f {ksr_fname} -i {now} -e +1h",
to_file=skr_fname,
)
check_signedkeyresponse(
skr_fname, FASTCONFIG, zone, ksks, zsks, now, until, refresh
)
# add zone
ns1.rndc(
f"addzone {zone} "
+ "{ type primary; file "
+ f'"{zone}.db"; dnssec-policy {policy}; '
+ "};",
)
# import skr
shutil.copyfile(skr_fname, f"ns1/{skr_fname}")
ns1.rndc(f"skr -import {skr_fname} {zone}")
# test zone is correctly signed
# - check rndc dnssec -status output
isctest.kasp.check_dnssecstatus(ns1, zone, zsks, policy=policy, verbose=True)
# - dnssec_verify
isctest.kasp.check_dnssec_verify(ns1, zone)
# - check keys
check_keys(zsks, lifetime, FASTCONFIG, with_state=True)
# - check apex
isctest.kasp.check_apex(ns1, zone, ksks, zsks, offline_ksk=True)
# - check subdomain