chg: usr: dnssec-ksr now supports KSK rollovers

The tool 'dnssec-ksr' now allows for KSK generation, as well as planned KSK rollovers. When signing a bundle from a Key Signing Request (KSR), only the key that is active in that time frame is being used for signing. Also, the CDS and CDNSKEY records are now added and removed at the correct time.

Closes #4697 

Closes #4705

Merge branch '4705-dnssec-ksr-only-sign-with-active-ksks' into 'main'

See merge request isc-projects/bind9!9452
This commit is contained in:
Matthijs Mekking 2024-11-01 15:29:37 +00:00
commit 675a7f0166
8 changed files with 462 additions and 154 deletions

View file

@ -25,6 +25,7 @@
#include <dns/callbacks.h>
#include <dns/dnssec.h>
#include <dns/fixedname.h>
#include <dns/keymgr.h>
#include <dns/keyvalues.h>
#include <dns/rdataclass.h>
#include <dns/rdatalist.h>
@ -61,16 +62,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;
@ -139,6 +143,15 @@ usage(int ret) {
exit(ret);
}
static isc_stdtime_t
between(isc_stdtime_t t, isc_stdtime_t start, isc_stdtime_t end) {
isc_stdtime_t r = end;
if (t > 0 && t > start && t < end) {
r = t;
}
return (r);
}
static void
checkparams(ksr_ctx_t *ksr, const char *command) {
if (ksr->configfile == NULL) {
@ -239,6 +252,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);
@ -246,6 +260,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);
}
@ -313,9 +328,9 @@ 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;
@ -327,9 +342,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)) {
@ -420,18 +441,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) {
@ -468,15 +489,28 @@ 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,
(inception == ksr->start));
}
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;
@ -610,12 +644,13 @@ fail:
return (next_bundle);
}
static void
static isc_stdtime_t
sign_rrset(ksr_ctx_t *ksr, isc_stdtime_t inception, isc_stdtime_t expiration,
dns_rdataset_t *rrset, dns_dnsseckeylist_t *keys) {
dns_rdatalist_t *rrsiglist = NULL;
dns_rdataset_t rrsigset = DNS_RDATASET_INIT;
isc_result_t ret;
isc_stdtime_t next_bundle = expiration;
UNUSED(ksr);
@ -659,6 +694,25 @@ sign_rrset(ksr_ctx_t *ksr, isc_stdtime_t inception, isc_stdtime_t expiration,
unsigned char rdatabuf[SIG_FORMATSIZE];
isc_stdtime_t clockskew = inception - 3600;
isc_stdtime_t pub = 0, act = 0, inact = 0, del = 0;
/* Determine next bundle. */
(void)dst_key_gettime(dk->key, DST_TIME_PUBLISH, &pub);
(void)dst_key_gettime(dk->key, DST_TIME_ACTIVATE, &act);
(void)dst_key_gettime(dk->key, DST_TIME_INACTIVE, &inact);
(void)dst_key_gettime(dk->key, DST_TIME_DELETE, &del);
next_bundle = between(pub, inception, next_bundle);
next_bundle = between(act, inception, next_bundle);
next_bundle = between(inact, inception, next_bundle);
next_bundle = between(del, inception, next_bundle);
if (act > inception) {
continue;
}
if (inact != 0 && inception >= inact) {
continue;
}
rrsig = isc_mem_get(mctx, sizeof(*rrsig));
dns_rdata_init(rrsig);
isc_buffer_init(&buf, rdatabuf, sizeof(rdatabuf));
@ -680,21 +734,25 @@ sign_rrset(ksr_ctx_t *ksr, isc_stdtime_t inception, isc_stdtime_t expiration,
dns_rdatalist_tordataset(rrsiglist, &rrsigset);
print_rdata(&rrsigset);
freerrset(&rrsigset);
return (next_bundle);
}
/*
* Create the DNSKEY, CDS, and CDNSKEY records beloing to the KSKs
* listed in 'keys'.
*/
static void
create_ksk(ksr_ctx_t *ksr, dns_kasp_t *kasp, dns_dnsseckeylist_t *keys,
dns_rdataset_t *dnskeyset, dns_rdataset_t *cdnskeyset,
dns_rdataset_t *cdsset) {
static isc_stdtime_t
get_keymaterial(ksr_ctx_t *ksr, dns_kasp_t *kasp, isc_stdtime_t inception,
isc_stdtime_t next_inception, dns_dnsseckeylist_t *keys,
dns_rdataset_t *dnskeyset, dns_rdataset_t *cdnskeyset,
dns_rdataset_t *cdsset) {
dns_kasp_digestlist_t digests = dns_kasp_digests(kasp);
dns_rdatalist_t *dnskeylist = isc_mem_get(mctx, sizeof(*dnskeylist));
dns_rdatalist_t *cdnskeylist = isc_mem_get(mctx, sizeof(*cdnskeylist));
dns_rdatalist_t *cdslist = isc_mem_get(mctx, sizeof(*cdslist));
isc_result_t ret = ISC_R_SUCCESS;
dns_kasp_digestlist_t digests = dns_kasp_digests(kasp);
isc_stdtime_t next_bundle = next_inception;
dns_rdatalist_init(dnskeylist);
dnskeylist->rdclass = dns_rdataclass_in;
@ -714,31 +772,73 @@ create_ksk(ksr_ctx_t *ksr, dns_kasp_t *kasp, dns_dnsseckeylist_t *keys,
for (dns_dnsseckey_t *dk = ISC_LIST_HEAD(*keys); dk != NULL;
dk = ISC_LIST_NEXT(dk, link))
{
bool published = true;
isc_buffer_t buf;
isc_buffer_t *newbuf;
dns_rdata_t *rdata;
isc_region_t r;
isc_region_t rcds;
isc_stdtime_t pub = 0, del = 0;
unsigned char kskbuf[DST_KEY_MAXSIZE];
unsigned char cdnskeybuf[DST_KEY_MAXSIZE];
unsigned char cdsbuf[DNS_DS_BUFFERSIZE];
/* KSK */
newbuf = NULL;
rdata = isc_mem_get(mctx, sizeof(*rdata));
dns_rdata_init(rdata);
(void)dst_key_gettime(dk->key, DST_TIME_PUBLISH, &pub);
(void)dst_key_gettime(dk->key, DST_TIME_DELETE, &del);
next_bundle = between(pub, inception, next_bundle);
next_bundle = between(del, inception, next_bundle);
isc_buffer_init(&buf, kskbuf, sizeof(kskbuf));
CHECK(dst_key_todns(dk->key, &buf));
isc_buffer_usedregion(&buf, &r);
isc_buffer_allocate(mctx, &newbuf, r.length);
isc_buffer_putmem(newbuf, r.base, r.length);
isc_buffer_usedregion(newbuf, &r);
dns_rdata_fromregion(rdata, dns_rdataclass_in,
dns_rdatatype_dnskey, &r);
ISC_LIST_APPEND(dnskeylist->rdata, rdata, link);
ISC_LIST_APPEND(cleanup_list, newbuf, link);
isc_buffer_clear(newbuf);
if (pub > inception) {
published = false;
}
if (del != 0 && inception >= del) {
published = false;
}
if (published) {
newbuf = NULL;
rdata = isc_mem_get(mctx, sizeof(*rdata));
dns_rdata_init(rdata);
isc_buffer_init(&buf, kskbuf, sizeof(kskbuf));
CHECK(dst_key_todns(dk->key, &buf));
isc_buffer_usedregion(&buf, &r);
isc_buffer_allocate(mctx, &newbuf, r.length);
isc_buffer_putmem(newbuf, r.base, r.length);
isc_buffer_usedregion(newbuf, &r);
dns_rdata_fromregion(rdata, dns_rdataclass_in,
dns_rdatatype_dnskey, &r);
ISC_LIST_APPEND(dnskeylist->rdata, rdata, link);
ISC_LIST_APPEND(cleanup_list, newbuf, link);
isc_buffer_clear(newbuf);
}
published = true;
if (dns_kasp_cdnskey(kasp) || !ISC_LIST_EMPTY(digests)) {
pub = 0;
del = 0;
(void)dst_key_gettime(dk->key, DST_TIME_SYNCPUBLISH,
&pub);
(void)dst_key_gettime(dk->key, DST_TIME_SYNCDELETE,
&del);
next_bundle = between(pub, inception, next_bundle);
next_bundle = between(del, inception, next_bundle);
if (pub != 0 && pub > inception) {
published = false;
}
if (del != 0 && inception >= del) {
published = false;
}
} else {
published = false;
}
if (!published) {
continue;
}
/* CDNSKEY */
newbuf = NULL;
@ -792,35 +892,98 @@ create_ksk(ksr_ctx_t *ksr, dns_kasp_t *kasp, dns_dnsseckeylist_t *keys,
dns_rdatalist_tordataset(dnskeylist, dnskeyset);
dns_rdatalist_tordataset(cdnskeylist, cdnskeyset);
dns_rdatalist_tordataset(cdslist, cdsset);
return;
return (next_bundle);
fail:
fatal("failed to create KSK/CDS/CDNSKEY");
return (0);
}
static void
sign_bundle(ksr_ctx_t *ksr, isc_stdtime_t inception,
isc_stdtime_t next_inception, dns_rdatalist_t *rdatalist,
dns_rdataset_t *cds, dns_rdataset_t *cdnskey,
sign_bundle(ksr_ctx_t *ksr, dns_kasp_t *kasp, isc_stdtime_t inception,
isc_stdtime_t next_inception, dns_rdatalist_t *zsklist,
dns_dnsseckeylist_t *keys) {
dns_rdataset_t rrset = DNS_RDATASET_INIT;
isc_stdtime_t expiration;
isc_stdtime_t expiration = inception + ksr->sigvalidity;
isc_stdtime_t next_bundle = next_inception;
dns_rdataset_t zsk;
dns_rdataset_init(&zsk);
dns_rdatalist_tordataset(zsklist, &zsk);
dns_rdataset_init(&rrset);
dns_rdatalist_tordataset(rdatalist, &rrset);
expiration = inception + ksr->sigvalidity;
while (inception <= next_inception) {
sign_rrset(ksr, inception, expiration, &rrset, keys);
if (dns_rdataset_count(cdnskey) > 0) {
sign_rrset(ksr, inception, expiration, cdnskey, keys);
isc_stdtime_t next_time = next_bundle;
/* DNSKEY RRset */
dns_rdatalist_t *dnskeylist;
dnskeylist = isc_mem_get(mctx, sizeof(*dnskeylist));
dns_rdatalist_init(dnskeylist);
dnskeylist->rdclass = dns_rdataclass_in;
dnskeylist->type = dns_rdatatype_dnskey;
dnskeylist->ttl = ksr->ttl;
dns_rdataset_t ksk, cdnskey, cds, rrset;
dns_rdataset_init(&ksk);
dns_rdataset_init(&cdnskey);
dns_rdataset_init(&cds);
dns_rdataset_init(&rrset);
next_time = get_keymaterial(ksr, kasp, inception, next_time,
keys, &ksk, &cdnskey, &cds);
if (next_bundle > next_time) {
next_bundle = next_time;
}
if (dns_rdataset_count(cds) > 0) {
sign_rrset(ksr, inception, expiration, cds, keys);
for (isc_result_t r = dns_rdatalist_first(&ksk);
r == ISC_R_SUCCESS; r = dns_rdatalist_next(&ksk))
{
dns_rdata_t *clone = isc_mem_get(mctx, sizeof(*clone));
dns_rdata_init(clone);
dns_rdatalist_current(&ksk, clone);
ISC_LIST_APPEND(dnskeylist->rdata, clone, link);
}
for (isc_result_t r = dns_rdatalist_first(&zsk);
r == ISC_R_SUCCESS; r = dns_rdatalist_next(&zsk))
{
dns_rdata_t *clone = isc_mem_get(mctx, sizeof(*clone));
dns_rdata_init(clone);
dns_rdatalist_current(&zsk, clone);
ISC_LIST_APPEND(dnskeylist->rdata, clone, link);
}
dns_rdatalist_tordataset(dnskeylist, &rrset);
next_time = sign_rrset(ksr, inception, expiration, &rrset,
keys);
if (next_bundle > next_time) {
next_bundle = next_time;
}
freerrset(&ksk);
freerrset(&rrset);
/* CDNSKEY */
if (dns_rdataset_count(&cdnskey) > 0) {
(void)sign_rrset(ksr, inception, expiration, &cdnskey,
keys);
}
freerrset(&cdnskey);
/* CDS */
if (dns_rdataset_count(&cds) > 0) {
(void)sign_rrset(ksr, inception, expiration, &cds,
keys);
}
freerrset(&cds);
/* Next response bundle. */
inception = expiration - ksr->sigrefresh;
if (inception > next_bundle) {
inception = next_bundle;
}
expiration = inception + ksr->sigvalidity;
next_bundle = expiration;
}
freerrset(&rrset);
freerrset(&zsk);
}
static isc_result_t
@ -907,9 +1070,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);
@ -920,7 +1086,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;
@ -928,7 +1094,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);
@ -1007,9 +1173,6 @@ sign(ksr_ctx_t *ksr) {
dns_dnsseckeylist_t keys;
dns_kasp_t *kasp = NULL;
dns_rdatalist_t *rdatalist = NULL;
dns_rdataset_t ksk = DNS_RDATASET_INIT;
dns_rdataset_t cdnskey = DNS_RDATASET_INIT;
dns_rdataset_t cds = DNS_RDATASET_INIT;
isc_result_t ret;
isc_stdtime_t inception;
isc_lex_t *lex = NULL;
@ -1042,9 +1205,6 @@ sign(ksr_ctx_t *ksr) {
isc_result_totext(ret));
}
/* KSK, CDS and CDNSKEY */
create_ksk(ksr, kasp, &keys, &ksk, &cdnskey, &cds);
for (ret = isc_lex_gettoken(lex, opt, &token); ret == ISC_R_SUCCESS;
ret = isc_lex_gettoken(lex, opt, &token))
{
@ -1094,8 +1254,8 @@ sign(ksr_ctx_t *ksr) {
if (have_bundle) {
/* Sign previous bundle */
sign_bundle(ksr, inception, next_inception,
rdatalist, &cds, &cdnskey, &keys);
sign_bundle(ksr, kasp, inception,
next_inception, rdatalist, &keys);
fprintf(stdout, "\n");
}
@ -1105,15 +1265,7 @@ sign(ksr_ctx_t *ksr) {
rdatalist->rdclass = dns_rdataclass_in;
rdatalist->type = dns_rdatatype_dnskey;
rdatalist->ttl = ksr->ttl;
for (isc_result_t r = dns_rdatalist_first(&ksk);
r == ISC_R_SUCCESS; r = dns_rdatalist_next(&ksk))
{
dns_rdata_t *clone =
isc_mem_get(mctx, sizeof(*clone));
dns_rdata_init(clone);
dns_rdatalist_current(&ksk, clone);
ISC_LIST_APPEND(rdatalist->rdata, clone, link);
}
inception = next_inception;
have_bundle = true;
@ -1172,8 +1324,7 @@ sign(ksr_ctx_t *ksr) {
/* Final bundle */
if (have_bundle && rdatalist != NULL) {
sign_bundle(ksr, inception, ksr->end, rdatalist, &cds, &cdnskey,
&keys);
sign_bundle(ksr, kasp, inception, ksr->end, rdatalist, &keys);
} else {
fatal("bad KSR file %s(%lu): no bundles", ksr->file,
isc_lex_getsourceline(lex));
@ -1185,11 +1336,6 @@ sign(ksr_ctx_t *ksr) {
timestr, PACKAGE_VERSION);
fail:
/* Clean up */
freerrset(&ksk);
freerrset(&cdnskey);
freerrset(&cds);
isc_lex_destroy(&lex);
cleanup(&keys, kasp);
}
@ -1212,7 +1358,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':
@ -1249,6 +1395,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;

View file

@ -21,7 +21,7 @@ dnssec-ksr - Create signed key response (SKR) files for offline KSK setups
Synopsis
~~~~~~~~
:program:`dnssec-ksr` [**-e** date/offset] [**-F**] [**-h**] [**-i** date/offset] [**-K** directory] [**-k** policy] [**-l** file] [**-V**] [**-v** level] {command} {zone}
:program:`dnssec-ksr` [**-e** date/offset] [**-F**] [**-f** file] [**-h**] [**-i** date/offset] [**-K** directory] [**-k** policy] [**-l** file] [**-o**] [**-V**] [**-v** level] {command} {zone}
Description
~~~~~~~~~~~
@ -51,6 +51,10 @@ Options
mode if the underlying crytographic library supports running in FIPS
mode.
.. option:: -f
This option sets the SKR file to be signed when issuing a ``sign`` command.
.. option:: -h
This option prints a short summary of the options and arguments to
@ -76,6 +80,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.
@ -98,9 +107,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
@ -123,7 +131,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":
::
@ -136,7 +144,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:
@ -144,7 +153,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
~~~~~~~~

View file

@ -431,8 +431,11 @@ def _check_dnskeys(dnskeys, keys, cdnskey=False):
has_dnskey = True
break
assert has_dnskey
numkeys += 1
if not cdnskey:
assert has_dnskey
if has_dnskey:
numkeys += 1
return numkeys
@ -541,17 +544,17 @@ def check_apex(server, zone, ksks, zsks):
# test cdnskey query
cdnskeys, rrsigs = _query_rrset(server, fqdn, dns.rdatatype.CDNSKEY)
assert len(cdnskeys) > 0
check_dnskeys(cdnskeys, ksks, zsks, cdnskey=True)
assert len(rrsigs) > 0
check_signatures(rrsigs, dns.rdatatype.CDNSKEY, fqdn, ksks, zsks)
if len(cdnskeys) > 0:
assert len(rrsigs) > 0
check_signatures(rrsigs, dns.rdatatype.CDNSKEY, fqdn, ksks, zsks)
# test cds query
cds, rrsigs = _query_rrset(server, fqdn, dns.rdatatype.CDS)
assert len(cds) > 0
check_cds(cds, ksks)
assert len(rrsigs) > 0
check_signatures(rrsigs, dns.rdatatype.CDS, fqdn, ksks, zsks)
if len(cds) > 0:
assert len(rrsigs) > 0
check_signatures(rrsigs, dns.rdatatype.CDS, fqdn, ksks, zsks)
def check_subdomain(server, zone, ksks, zsks):

View file

@ -85,3 +85,11 @@ dnssec-policy "two-tone" {
zsk lifetime P3M algorithm @DEFAULT_ALGORITHM@;
};
};
dnssec-policy "ksk-roll" {
offline-ksk yes;
keys {
ksk lifetime P6M algorithm @DEFAULT_ALGORITHM@;
zsk lifetime unlimited algorithm @DEFAULT_ALGORITHM@;
};
};

View file

@ -26,3 +26,4 @@ cp template.db.in last-bundle.test.db
cp template.db.in in-the-middle.test.db
cp template.db.in unlimited.test.db
cp template.db.in two-tone.test.db
cp template.db.in ksk-roll.test.db

View file

@ -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
@ -311,10 +324,17 @@ def check_signedkeyresponse(
line_no += 1
# expect cdnskey
have_cdnskey = False
if cdnskey:
for key in sorted(ksks):
published = key.get_timing("Publish")
removed = key.get_timing("Delete", must_exist=False)
published = key.get_timing("SyncPublish")
if between(published, inception, next_bundle):
next_bundle = published
removed = key.get_timing("SyncDelete", must_exist=False)
if between(removed, inception, next_bundle):
next_bundle = removed
if published > inception:
continue
if removed is not None and inception >= removed:
@ -323,7 +343,9 @@ def check_signedkeyresponse(
# the cdnskey of this ksk must be in the ksr
assert key.dnskey_equals(lines[line_no], cdnskey=True)
line_no += 1
have_cdnskey = True
if have_cdnskey:
# expect rrsig(cdnskey)
for key in sorted(ksks):
active = key.get_timing("Activate")
@ -341,10 +363,17 @@ def check_signedkeyresponse(
line_no += 1
# expect cds
have_cds = False
if cds != "":
for key in sorted(ksks):
published = key.get_timing("Publish")
removed = key.get_timing("Delete", must_exist=False)
published = key.get_timing("SyncPublish")
if between(published, inception, next_bundle):
next_bundle = published
removed = key.get_timing("SyncDelete", must_exist=False)
if between(removed, inception, next_bundle):
next_bundle = removed
if published > inception:
continue
if removed is not None and inception >= removed:
@ -355,7 +384,9 @@ def check_signedkeyresponse(
for alg in expected_cds:
assert key.cds_equals(lines[line_no], alg.strip())
line_no += 1
have_cds = True
if have_cds:
# expect rrsig(cds)
for key in sorted(ksks):
active = key.get_timing("Activate")
@ -399,7 +430,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 +455,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 +644,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 +723,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 +804,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 +877,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 +994,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")
@ -1050,3 +1103,77 @@ def test_ksr_twotone(servers):
isctest.kasp.check_apex(ns1, zone, ksks, zsks)
# - check subdomain
isctest.kasp.check_subdomain(ns1, zone, ksks, zsks)
def test_ksr_kskroll(servers):
zone = "ksk-roll.test"
policy = "ksk-roll"
n = 1
# create ksk
kskdir = "ns1/offline"
out, _ = ksr(zone, policy, "keygen", options=f"-K {kskdir} -i now -e +1y -o")
ksks = keystr_to_keylist(out, kskdir)
assert len(ksks) == 2
lifetime = timedelta(days=31 * 6)
check_keys(ksks, lifetime)
# 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")
zsks = keystr_to_keylist(out, zskdir)
assert len(zsks) == 1
check_keys(zsks, None)
# check that 'dnssec-ksr request' creates correct ksr
now = zsks[0].get_timing("Created")
until = now + timedelta(days=365)
out, _ = ksr(zone, policy, "request", options=f"-K {zskdir} -i {now} -e +1y")
fname = f"{zone}.ksr.{n}"
with open(fname, "w", encoding="utf-8") as file:
file.write(out)
check_keysigningrequest(out, zsks, now, until)
# check that 'dnssec-ksr sign' creates correct skr
out, _ = ksr(
zone, policy, "sign", options=f"-K {kskdir} -f {fname} -i {now} -e +1y"
)
skrfile = f"{zone}.skr.{n}"
with open(skrfile, "w", encoding="utf-8") as file:
file.write(out)
refresh = -432000 # 5 days
check_signedkeyresponse(out, zone, ksks, zsks, now, until, refresh)
# add zone
ns1 = servers["ns1"]
ns1.rndc(
f"addzone {zone} "
+ "{ type primary; file "
+ f'"{zone}.db"; dnssec-policy {policy}; '
+ "};",
log=False,
)
# import skr
shutil.copyfile(skrfile, f"ns1/{skrfile}")
ns1.rndc(f"skr -import {skrfile} {zone}", log=False)
# test zone is correctly signed
# - check rndc dnssec -status output
isctest.kasp.check_dnssecstatus(ns1, zone, zsks, policy=policy)
# - zone is signed
isctest.kasp.check_zone_is_signed(ns1, zone)
# - dnssec_verify
isctest.kasp.check_dnssec_verify(ns1, zone)
# - check keys
check_keys(zsks, None, with_state=True)
# - check apex
isctest.kasp.check_apex(ns1, zone, ksks, zsks)
# - check subdomain
isctest.kasp.check_subdomain(ns1, zone, ksks, zsks)

View file

@ -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,

View file

@ -163,26 +163,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) {
@ -196,7 +195,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);
}
/*
@ -1862,7 +1861,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 {
/*
@ -1894,7 +1893,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.