diff --git a/bin/dnssec/dnssec-ksr.c b/bin/dnssec/dnssec-ksr.c index 5705842042..376124a7d2 100644 --- a/bin/dnssec/dnssec-ksr.c +++ b/bin/dnssec/dnssec-ksr.c @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -63,16 +64,19 @@ struct ksr_ctx { bool setstart; bool setend; /* keygen */ + bool ksk; dns_ttl_t ttl; dns_secalg_t alg; int size; time_t lifetime; + time_t parentpropagation; time_t propagation; time_t publishsafety; time_t retiresafety; time_t sigrefresh; time_t sigvalidity; time_t signdelay; + time_t ttlds; time_t ttlsig; }; typedef struct ksr_ctx ksr_ctx_t; @@ -243,6 +247,7 @@ get_dnskeys(ksr_ctx_t *ksr, dns_dnsseckeylist_t *keys) { static void setcontext(ksr_ctx_t *ksr, dns_kasp_t *kasp) { + ksr->parentpropagation = dns_kasp_parentpropagationdelay(kasp); ksr->propagation = dns_kasp_zonepropagationdelay(kasp); ksr->publishsafety = dns_kasp_publishsafety(kasp); ksr->retiresafety = dns_kasp_retiresafety(kasp); @@ -250,6 +255,7 @@ setcontext(ksr_ctx_t *ksr, dns_kasp_t *kasp) { ksr->sigrefresh = dns_kasp_sigrefresh(kasp); ksr->signdelay = dns_kasp_signdelay(kasp); ksr->ttl = dns_kasp_dnskeyttl(kasp); + ksr->ttlds = dns_kasp_dsttl(kasp); ksr->ttlsig = dns_kasp_zonemaxttl(kasp, true); } @@ -317,12 +323,13 @@ freerrset(dns_rdataset_t *rdataset) { } static void -create_zsk(ksr_ctx_t *ksr, dns_kasp_key_t *kaspkey, dns_dnsseckeylist_t *keys, - isc_stdtime_t inception, isc_stdtime_t active, - isc_stdtime_t *expiration) { +create_key(ksr_ctx_t *ksr, dns_kasp_t *kasp, dns_kasp_key_t *kaspkey, + dns_dnsseckeylist_t *keys, isc_stdtime_t inception, + isc_stdtime_t active, isc_stdtime_t *expiration) { bool conflict = false; bool freekey = false; bool show_progress = true; + bool first = true; char algstr[DNS_SECALG_FORMATSIZE]; char filename[PATH_MAX + 1]; char timestr[26]; /* Minimal buf as per ctime_r() spec. */ @@ -331,9 +338,15 @@ create_zsk(ksr_ctx_t *ksr, dns_kasp_key_t *kaspkey, dns_dnsseckeylist_t *keys, isc_buffer_t buf; isc_result_t ret; isc_stdtime_t prepub; + uint16_t flags = DNS_KEYOWNER_ZONE; isc_stdtime_tostring(inception, timestr, sizeof(timestr)); + /* ZSK or KSK? */ + if (ksr->ksk) { + flags |= DNS_KEYFLAG_KSK; + } + /* Check algorithm and size. */ dns_secalg_format(ksr->alg, algstr, sizeof(algstr)); if (!dst_algorithm_supported(ksr->alg)) { @@ -407,6 +420,7 @@ create_zsk(ksr_ctx_t *ksr, dns_kasp_key_t *kaspkey, dns_dnsseckeylist_t *keys, "Selecting key pair for bundle %s: ", timestr); fflush(stderr); } + first = false; key = dk->key; *expiration = inact; goto output; @@ -424,18 +438,18 @@ create_zsk(ksr_ctx_t *ksr, dns_kasp_key_t *kaspkey, dns_dnsseckeylist_t *keys, ret = dns_keystore_keygen( ksr->keystore, name, ksr->policy, dns_rdataclass_in, mctx, ksr->alg, ksr->size, - DNS_KEYOWNER_ZONE, &key); + flags, &key); } else if (show_progress) { - ret = dst_key_generate( - name, ksr->alg, ksr->size, 0, DNS_KEYOWNER_ZONE, - DNS_KEYPROTO_DNSSEC, dns_rdataclass_in, NULL, - mctx, &key, &progress); + ret = dst_key_generate(name, ksr->alg, ksr->size, 0, + flags, DNS_KEYPROTO_DNSSEC, + dns_rdataclass_in, NULL, mctx, + &key, &progress); fflush(stderr); } else { - ret = dst_key_generate( - name, ksr->alg, ksr->size, 0, DNS_KEYOWNER_ZONE, - DNS_KEYPROTO_DNSSEC, dns_rdataclass_in, NULL, - mctx, &key, NULL); + ret = dst_key_generate(name, ksr->alg, ksr->size, 0, + flags, DNS_KEYPROTO_DNSSEC, + dns_rdataclass_in, NULL, mctx, + &key, NULL); } if (ret != ISC_R_SUCCESS) { @@ -472,15 +486,27 @@ create_zsk(ksr_ctx_t *ksr, dns_kasp_key_t *kaspkey, dns_dnsseckeylist_t *keys, prepub = ksr->ttl + ksr->publishsafety + ksr->propagation; dst_key_setttl(key, ksr->ttl); dst_key_setnum(key, DST_NUM_LIFETIME, ksr->lifetime); - dst_key_setbool(key, DST_BOOL_KSK, false); - dst_key_setbool(key, DST_BOOL_ZSK, true); + dst_key_setbool(key, DST_BOOL_KSK, ksr->ksk); + dst_key_setbool(key, DST_BOOL_ZSK, !ksr->ksk); dst_key_settime(key, DST_TIME_CREATED, ksr->now); dst_key_settime(key, DST_TIME_PUBLISH, (active - prepub)); dst_key_settime(key, DST_TIME_ACTIVATE, active); + if (ksr->ksk) { + dns_keymgr_settime_syncpublish(key, kasp, first); + } + if (ksr->lifetime > 0) { isc_stdtime_t inactive = (active + ksr->lifetime); - isc_stdtime_t remove = ksr->ttlsig + ksr->propagation + - ksr->retiresafety + ksr->signdelay; + isc_stdtime_t remove; + + if (ksr->ksk) { + remove = ksr->ttlds + ksr->parentpropagation + + ksr->retiresafety; + dst_key_settime(key, DST_TIME_SYNCDELETE, inactive); + } else { + remove = ksr->ttlsig + ksr->propagation + + ksr->retiresafety + ksr->signdelay; + } dst_key_settime(key, DST_TIME_INACTIVE, inactive); dst_key_settime(key, DST_TIME_DELETE, (inactive + remove)); *expiration = inactive; @@ -496,6 +522,8 @@ create_zsk(ksr_ctx_t *ksr, dns_kasp_key_t *kaspkey, dns_dnsseckeylist_t *keys, isc_result_totext(ret)); } + first = false; + output: isc_buffer_clear(&buf); ret = dst_key_buildfilename(key, 0, NULL, &buf); @@ -911,9 +939,12 @@ keygen(ksr_ctx_t *ksr) { for (dns_kasp_key_t *kk = ISC_LIST_HEAD(dns_kasp_keys(kasp)); kk != NULL; kk = ISC_LIST_NEXT(kk, link)) { - if (dns_kasp_key_ksk(kk)) { + if (dns_kasp_key_ksk(kk) && !ksr->ksk) { /* only ZSKs allowed */ continue; + } else if (dns_kasp_key_zsk(kk) && ksr->ksk) { + /* only KSKs allowed */ + continue; } ksr->alg = dns_kasp_key_algorithm(kk); ksr->lifetime = dns_kasp_key_lifetime(kk); @@ -924,7 +955,7 @@ keygen(ksr_ctx_t *ksr) { for (isc_stdtime_t inception = ksr->start, act = ksr->start; inception < ksr->end; inception += ksr->lifetime) { - create_zsk(ksr, kk, &keys, inception, act, &act); + create_key(ksr, kasp, kk, &keys, inception, act, &act); if (ksr->lifetime == 0) { /* unlimited lifetime, but not infinite loop */ break; @@ -932,7 +963,7 @@ keygen(ksr_ctx_t *ksr) { } } if (noop) { - fatal("policy '%s' has no zsks", ksr->policy); + fatal("no keys created for policy '%s'", ksr->policy); } /* Cleanup */ cleanup(&keys, kasp); @@ -1216,7 +1247,7 @@ main(int argc, char *argv[]) { isc_commandline_errprint = false; -#define OPTIONS "E:e:Ff:hi:K:k:l:v:V" +#define OPTIONS "E:e:Ff:hi:K:k:l:ov:V" while ((ch = isc_commandline_parse(argc, argv, OPTIONS)) != -1) { switch (ch) { case 'E': @@ -1253,6 +1284,9 @@ main(int argc, char *argv[]) { case 'l': ksr.configfile = isc_commandline_argument; break; + case 'o': + ksr.ksk = true; + break; case 'V': version(program); break; diff --git a/bin/dnssec/dnssec-ksr.rst b/bin/dnssec/dnssec-ksr.rst index f31e8db13c..3dcb8aaba3 100644 --- a/bin/dnssec/dnssec-ksr.rst +++ b/bin/dnssec/dnssec-ksr.rst @@ -21,7 +21,7 @@ dnssec-ksr - Create signed key response (SKR) files for offline KSK setups Synopsis ~~~~~~~~ -:program:`dnssec-ksr` [**-E** engine] [**-e** date/offset] [**-F**] [**-f** file] [**-h**] [**-i** date/offset] [**-K** directory] [**-k** policy] [**-l** file] [**-V**] [**-v** level] {command} {zone} +:program:`dnssec-ksr` [**-E** engine] [**-e** date/offset] [**-F**] [**-f** file] [**-h**] [**-i** date/offset] [**-K** directory] [**-k** policy] [**-l** file] [**-o**] [**-V**] [**-v** level] {command} {zone} Description ~~~~~~~~~~~ @@ -88,6 +88,11 @@ Options This option provides a configuration file that contains a ``dnssec-policy`` statement (matching the policy set with :option:`-k`). +.. option:: -o + + Normally when pregenerating keys, ZSKs are created. When this option is + set, create KSKs instead. + .. option:: -V This option prints version information. @@ -110,9 +115,8 @@ Commands .. option:: keygen - Pregenerate a number of zone signing keys (ZSKs), given a DNSSEC policy and - an interval. The number of generated keys depends on the interval and the - ZSK lifetime. + Pregenerate a number of keys, given a DNSSEC policy and an interval. The + number of generated keys depends on the interval and the key lifetime. .. option:: request @@ -135,7 +139,7 @@ occurred. Examples ~~~~~~~~ -When you need to generate keys for the zone "example.com" for the next year, +When you need to generate ZSKs for the zone "example.com" for the next year, given a ``dnssec-policy`` named "mypolicy": :: @@ -148,7 +152,8 @@ Creating a KSR for the same zone and period can be done with: dnssec-ksr -i now -e +1y -k mypolicy -l named.conf request example.com > ksr.txt -Typically you would now transfer the KSR to the system that has access to the KSK. +Typically you would now transfer the KSR to the system that has access to +the KSK. Signing the KSR created above can be done with: @@ -156,7 +161,8 @@ Signing the KSR created above can be done with: dnssec-ksr -i now -e +1y -k kskpolicy -l named.conf -f ksr.txt sign example.com -Make sure that the DNSSEC parameters in ``kskpolicy`` match those in ``mypolicy``. +Make sure that the DNSSEC parameters in ``kskpolicy`` match those +in ``mypolicy``. See Also ~~~~~~~~ diff --git a/bin/tests/system/ksr/tests_ksr.py b/bin/tests/system/ksr/tests_ksr.py index 793df81c4c..3790003f33 100644 --- a/bin/tests/system/ksr/tests_ksr.py +++ b/bin/tests/system/ksr/tests_ksr.py @@ -55,28 +55,6 @@ def keystr_to_keylist(keystr: str, keydir: Optional[str] = None) -> List[Key]: return [Key(name, keydir) for name in keystr.split()] -def keygen(zone, policy, keydir, when="now"): - keygen_command = [ - os.environ.get("KEYGEN"), - "-l", - "ns1/named.conf", - "-fK", - "-K", - keydir, - "-k", - policy, - "-P", - when, - "-A", - when, - "-P", - "sync", - when, - zone, - ] - return isctest.run.cmd(keygen_command, log_stdout=True).stdout.decode("utf-8") - - def ksr(zone, policy, action, options="", raise_on_exception=True): ksr_command = [ os.environ.get("KSR"), @@ -123,12 +101,22 @@ def check_keys( # retired: zsk-lifetime if lifetime is not None: retired = active + lifetime - # removed: ttlsig + retire-safety + sign-delay + propagation - removed = retired + timedelta(days=10, hours=1, minutes=5) + + if key.is_ksk(): + # removed: ttlds + retire-safety + parent-propagation + removed = retired + timedelta(days=1, hours=2) + else: + # removed: ttlsig + retire-safety + sign-delay + propagation + removed = retired + timedelta(days=10, hours=1, minutes=5) else: retired = None removed = None + goal = "hidden" + state_dnskey = "hidden" + state_zrrsig = "hidden" + state_krrsig = "hidden" + state_ds = "hidden" if retired is None or between(now, published, retired): goal = "omnipresent" pubdelay = published + timedelta(hours=2, minutes=5) @@ -136,24 +124,34 @@ def check_keys( if between(now, published, pubdelay): state_dnskey = "rumoured" + state_krrsig = "rumoured" else: state_dnskey = "omnipresent" + state_krrsig = "omnipresent" - if between(now, active, signdelay): - state_zrrsig = "rumoured" + if key.is_ksk(): + state_ds = "hidden" else: - state_zrrsig = "omnipresent" - else: - goal = "hidden" - state_dnskey = "hidden" - state_zrrsig = "hidden" + if between(now, active, signdelay): + state_zrrsig = "rumoured" + else: + state_zrrsig = "omnipresent" with open(key.statefile, "r", encoding="utf-8") as file: metadata = file.read() assert f"Algorithm: {alg}" in metadata assert f"Length: {size}" in metadata - assert "KSK: no" in metadata - assert "ZSK: yes" in metadata + + if key.is_ksk(): + assert "KSK: yes" in metadata + else: + assert "KSK: no" in metadata + + if key.is_zsk(): + assert "ZSK: yes" in metadata + else: + assert "ZSK: no" in metadata + assert f"Published: {published}" in metadata assert f"Active: {active}" in metadata @@ -169,9 +167,18 @@ def check_keys( if with_state: assert f"GoalState: {goal}" in metadata assert f"DNSKEYState: {state_dnskey}" in metadata - assert f"ZRRSIGState: {state_zrrsig}" in metadata - assert "KRRSIGState:" not in metadata - assert "DSState:" not in metadata + + if key.is_ksk(): + assert f"KRRSIGState: {state_krrsig}" in metadata + assert f"DSState: {state_ds}" in metadata + else: + assert "KRRSIGState:" not in metadata + assert "DSState:" not in metadata + + if key.is_zsk(): + assert f"ZRRSIGState: {state_zrrsig}" in metadata + else: + assert "ZRRSIGState:" not in metadata num += 1 @@ -264,6 +271,9 @@ def check_signedkeyresponse( # expect ksks for key in sorted(ksks): published = key.get_timing("Publish") + if between(published, inception, next_bundle): + next_bundle = published + removed = key.get_timing("Delete", must_exist=False) if published > inception: @@ -271,6 +281,9 @@ def check_signedkeyresponse( if removed is not None and inception >= removed: continue + if between(removed, inception, next_bundle): + next_bundle = removed + # this ksk must be in the ksr assert key.dnskey_equals(lines[line_no]) line_no += 1 @@ -399,7 +412,7 @@ def test_ksr_errors(): _, err = ksr( "csk.test", "csk", "keygen", options="-K ns1 -e +2y", raise_on_exception=False ) - assert "dnssec-ksr: fatal: policy 'csk' has no zsks" in err + assert "dnssec-ksr: fatal: no keys created for policy 'csk'" in err # check that 'dnssec-ksr request' errors on missing end date _, err = ksr("common.test", "common", "request", raise_on_exception=False) @@ -424,10 +437,12 @@ def test_ksr_common(servers): # create ksk kskdir = "ns1/offline" - out = keygen(zone, policy, kskdir) + out, _ = ksr(zone, policy, "keygen", options=f"-K {kskdir} -i now -e +1y -o") ksks = keystr_to_keylist(out, kskdir) assert len(ksks) == 1 + check_keys(ksks, None) + # check that 'dnssec-ksr keygen' pregenerates right amount of keys out, _ = ksr(zone, policy, "keygen", options="-i now -e +1y") zsks = keystr_to_keylist(out) @@ -611,13 +626,13 @@ def test_ksr_lastbundle(servers): # create ksk kskdir = "ns1/offline" - now = KeyTimingMetadata.now() offset = -timedelta(days=365) - when = now + offset - timedelta(days=1) - out = keygen(zone, policy, kskdir, when=str(when)) + out, _ = ksr(zone, policy, "keygen", options=f"-K {kskdir} -i -1y -e +1d -o") ksks = keystr_to_keylist(out, kskdir) assert len(ksks) == 1 + check_keys(ksks, None, offset=offset) + # check that 'dnssec-ksr keygen' pregenerates right amount of keys zskdir = "ns1" out, _ = ksr(zone, policy, "keygen", options=f"-K {zskdir} -i -1y -e +1d") @@ -690,13 +705,13 @@ def test_ksr_inthemiddle(servers): # create ksk kskdir = "ns1/offline" - now = KeyTimingMetadata.now() offset = -timedelta(days=365) - when = now + offset - timedelta(days=1) - out = keygen(zone, policy, kskdir, when=str(when)) + out, _ = ksr(zone, policy, "keygen", options=f"-K {kskdir} -i -1y -e +1y -o") ksks = keystr_to_keylist(out, kskdir) assert len(ksks) == 1 + check_keys(ksks, None, offset=offset) + # check that 'dnssec-ksr keygen' pregenerates right amount of keys zskdir = "ns1" out, _ = ksr(zone, policy, "keygen", options=f"-K {zskdir} -i -1y -e +1y") @@ -771,7 +786,7 @@ def check_ksr_rekey_logs_error(server, zone, policy, offset, end): now = KeyTimingMetadata.now() then = now + offset until = now + end - out = keygen(zone, policy, kskdir, when=str(then)) + out, _ = ksr(zone, policy, "keygen", options=f"-K {kskdir} -i {then} -e {until} -o") ksks = keystr_to_keylist(out, kskdir) assert len(ksks) == 1 @@ -844,10 +859,12 @@ def test_ksr_unlimited(servers): # create ksk kskdir = "ns1/offline" - out = keygen(zone, policy, kskdir) + out, _ = ksr(zone, policy, "keygen", options=f"-K {kskdir} -i now -e +2y -o") ksks = keystr_to_keylist(out, kskdir) assert len(ksks) == 1 + check_keys(ksks, None) + # check that 'dnssec-ksr keygen' pregenerates right amount of keys zskdir = "ns1" out, _ = ksr(zone, policy, "keygen", options=f"-K {zskdir} -i now -e +2y") @@ -959,10 +976,28 @@ def test_ksr_twotone(servers): # create ksk kskdir = "ns1/offline" - out = keygen(zone, policy, kskdir) + out, _ = ksr(zone, policy, "keygen", options=f"-K {kskdir} -i now -e +1y -o") ksks = keystr_to_keylist(out, kskdir) assert len(ksks) == 2 + ksks_defalg = [] + ksks_altalg = [] + for ksk in ksks: + alg = ksk.get_metadata("Algorithm") + if alg == os.environ.get("DEFAULT_ALGORITHM_NUMBER"): + ksks_defalg.append(ksk) + elif alg == os.environ.get("ALTERNATIVE_ALGORITHM_NUMBER"): + ksks_altalg.append(ksk) + + assert len(ksks_defalg) == 1 + assert len(ksks_altalg) == 1 + + check_keys(ksks_defalg, None) + + alg = os.environ.get("ALTERNATIVE_ALGORITHM_NUMBER") + size = os.environ.get("ALTERNATIVE_BITS") + check_keys(ksks_altalg, None, alg, size) + # check that 'dnssec-ksr keygen' pregenerates right amount of keys zskdir = "ns1" out, _ = ksr(zone, policy, "keygen", options=f"-K {zskdir} -i now -e +1y") diff --git a/lib/dns/include/dns/keymgr.h b/lib/dns/include/dns/keymgr.h index 61cf3df7aa..deb007623f 100644 --- a/lib/dns/include/dns/keymgr.h +++ b/lib/dns/include/dns/keymgr.h @@ -24,6 +24,17 @@ ISC_LANG_BEGINDECLS +void +dns_keymgr_settime_syncpublish(dst_key_t *key, dns_kasp_t *kasp, bool first); +/*%< + * Set the SyncPublish time (when the DS may be submitted to the parent). + * If 'first' is true, also make sure that the zone signatures are omnipresent. + * + * Requires: + *\li 'key' is a valid DNSSEC key. + *\li 'kasp' is a valid DNSSEC policy. + */ + isc_result_t dns_keymgr_run(const dns_name_t *origin, dns_rdataclass_t rdclass, isc_mem_t *mctx, dns_dnsseckeylist_t *keyring, diff --git a/lib/dns/keymgr.c b/lib/dns/keymgr.c index 39e81e0d3d..8cabad1800 100644 --- a/lib/dns/keymgr.c +++ b/lib/dns/keymgr.c @@ -164,26 +164,25 @@ keymgr_settime_remove(dns_dnsseckey_t *key, dns_kasp_t *kasp) { * Set the SyncPublish time (when the DS may be submitted to the parent). * */ -static void -keymgr_settime_syncpublish(dns_dnsseckey_t *key, dns_kasp_t *kasp, bool first) { +void +dns_keymgr_settime_syncpublish(dst_key_t *key, dns_kasp_t *kasp, bool first) { isc_stdtime_t published, syncpublish; bool ksk = false; isc_result_t ret; REQUIRE(key != NULL); - REQUIRE(key->key != NULL); - ret = dst_key_gettime(key->key, DST_TIME_PUBLISH, &published); + ret = dst_key_gettime(key, DST_TIME_PUBLISH, &published); if (ret != ISC_R_SUCCESS) { return; } - ret = dst_key_getbool(key->key, DST_BOOL_KSK, &ksk); + ret = dst_key_getbool(key, DST_BOOL_KSK, &ksk); if (ret != ISC_R_SUCCESS || !ksk) { return; } - syncpublish = published + dst_key_getttl(key->key) + + syncpublish = published + dst_key_getttl(key) + dns_kasp_zonepropagationdelay(kasp) + dns_kasp_publishsafety(kasp); if (first) { @@ -197,7 +196,7 @@ keymgr_settime_syncpublish(dns_dnsseckey_t *key, dns_kasp_t *kasp, bool first) { syncpublish = zrrsig_present; } } - dst_key_settime(key->key, DST_TIME_SYNCPUBLISH, syncpublish); + dst_key_settime(key, DST_TIME_SYNCPUBLISH, syncpublish); } /* @@ -1863,7 +1862,7 @@ keymgr_key_rollover(dns_kasp_key_t *kaspkey, dns_dnsseckey_t *active_key, */ dst_key_settime(new_key->key, DST_TIME_PUBLISH, now); dst_key_settime(new_key->key, DST_TIME_ACTIVATE, now); - keymgr_settime_syncpublish(new_key, kasp, true); + dns_keymgr_settime_syncpublish(new_key->key, kasp, true); active = now; } else { /* @@ -1895,7 +1894,7 @@ keymgr_key_rollover(dns_kasp_key_t *kaspkey, dns_dnsseckey_t *active_key, } dst_key_settime(new_key->key, DST_TIME_PUBLISH, prepub); dst_key_settime(new_key->key, DST_TIME_ACTIVATE, active); - keymgr_settime_syncpublish(new_key, kasp, false); + dns_keymgr_settime_syncpublish(new_key->key, kasp, false); /* * Retire predecessor.