/* $NetBSD: dnssec-ksr.c,v 1.2 2025/01/26 16:24:32 christos Exp $ */ /* * 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. */ /*! \file */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "dnssectool.h" const char *program = "dnssec-ksr"; /* * Infrastructure */ static isc_log_t *lctx = NULL; static isc_mem_t *mctx = NULL; const char *engine = NULL; /* * The domain we are working on */ static const char *namestr = NULL; static dns_fixedname_t fname; static dns_name_t *name = NULL; /* * KSR context */ struct ksr_ctx { const char *policy; const char *configfile; const char *file; const char *keydir; dns_keystore_t *keystore; isc_stdtime_t now; isc_stdtime_t start; isc_stdtime_t end; 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; /* * These are set here for backwards compatibility. * They are raised to 2048 in FIPS mode. */ static int min_rsa = 1024; static int min_dh = 128; #define KSR_LINESIZE 1500 /* should be long enough for any DNSKEY record */ #define DATETIME_INDEX 25 #define MAXWIRE (64 * 1024) #define STR(t) ((t).value.as_textregion.base) #define READLINE(lex, opt, token) #define NEXTTOKEN(lex, opt, token) \ { \ ret = isc_lex_gettoken(lex, opt, token); \ if (ret != ISC_R_SUCCESS) \ goto cleanup; \ } #define BADTOKEN() \ { \ ret = ISC_R_UNEXPECTEDTOKEN; \ goto cleanup; \ } #define CHECK(r) \ ret = (r); \ if (ret != ISC_R_SUCCESS) { \ goto fail; \ } isc_bufferlist_t cleanup_list = ISC_LIST_INITIALIZER; static void usage(int ret) { fprintf(stderr, "Usage:\n"); fprintf(stderr, " %s options [options] \n", program); fprintf(stderr, "\n"); fprintf(stderr, "Version: %s\n", PACKAGE_VERSION); fprintf(stderr, "\n"); fprintf(stderr, "Options:\n"); fprintf(stderr, " -E : name of an OpenSSL engine to use\n"); fprintf(stderr, " -e : end date\n"); fprintf(stderr, " -F: FIPS mode\n"); fprintf(stderr, " -f: KSR file to sign\n"); fprintf(stderr, " -i : start date\n"); fprintf(stderr, " -K : key directory\n"); fprintf(stderr, " -k : name of a DNSSEC policy\n"); fprintf(stderr, " -l : file with dnssec-policy config\n"); fprintf(stderr, " -h: print usage and exit\n"); fprintf(stderr, " -V: print version information\n"); fprintf(stderr, " -v : set verbosity level\n"); fprintf(stderr, "\n"); fprintf(stderr, "Commands:\n"); fprintf(stderr, " keygen: pregenerate ZSKs\n"); fprintf(stderr, " request: create a Key Signing Request (KSR)\n"); fprintf(stderr, " sign: sign a KSR, creating a Signed Key " "Response (SKR)\n"); 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) { fatal("%s requires a configuration file", command); } if (ksr->policy == NULL) { fatal("%s requires a dnssec-policy", command); } if (!ksr->setend) { fatal("%s requires an end date", command); } if (!ksr->setstart) { ksr->start = ksr->now; } if (ksr->keydir == NULL) { ksr->keydir = "."; } } static void getkasp(ksr_ctx_t *ksr, dns_kasp_t **kasp) { cfg_parser_t *parser = NULL; cfg_obj_t *config = NULL; RUNTIME_CHECK(cfg_parser_create(mctx, lctx, &parser) == ISC_R_SUCCESS); if (cfg_parse_file(parser, ksr->configfile, &cfg_type_namedconf, &config) != ISC_R_SUCCESS) { fatal("unable to load dnssec-policy '%s' from '%s'", ksr->policy, ksr->configfile); } kasp_from_conf(config, mctx, lctx, ksr->policy, ksr->keydir, engine, kasp); if (*kasp == NULL) { fatal("failed to load dnssec-policy '%s'", ksr->policy); } if (ISC_LIST_EMPTY(dns_kasp_keys(*kasp))) { fatal("dnssec-policy '%s' has no keys configured", ksr->policy); } cfg_obj_destroy(parser, &config); cfg_parser_destroy(&parser); } static int keyalgtag_cmp(const void *k1, const void *k2) { dns_dnsseckey_t **key1 = (dns_dnsseckey_t **)k1; dns_dnsseckey_t **key2 = (dns_dnsseckey_t **)k2; if (dst_key_alg((*key1)->key) < dst_key_alg((*key2)->key)) { return -1; } else if (dst_key_alg((*key1)->key) > dst_key_alg((*key2)->key)) { return 1; } else if (dst_key_id((*key1)->key) < dst_key_id((*key2)->key)) { return -1; } else if (dst_key_id((*key1)->key) > dst_key_id((*key2)->key)) { return 1; } return 0; } static void get_dnskeys(ksr_ctx_t *ksr, dns_dnsseckeylist_t *keys) { dns_dnsseckeylist_t keys_read; dns_dnsseckey_t **keys_sorted; int i = 0, n = 0; isc_result_t ret; ISC_LIST_INIT(*keys); ISC_LIST_INIT(keys_read); ret = dns_dnssec_findmatchingkeys(name, NULL, ksr->keydir, NULL, ksr->now, mctx, &keys_read); if (ret != ISC_R_SUCCESS && ret != ISC_R_NOTFOUND) { fatal("failed to load existing keys from %s: %s", ksr->keydir, isc_result_totext(ret)); } /* Sort on keytag. */ for (dns_dnsseckey_t *dk = ISC_LIST_HEAD(keys_read); dk != NULL; dk = ISC_LIST_NEXT(dk, link)) { n++; } keys_sorted = isc_mem_cget(mctx, n, sizeof(dns_dnsseckey_t *)); for (dns_dnsseckey_t *dk = ISC_LIST_HEAD(keys_read); dk != NULL; dk = ISC_LIST_NEXT(dk, link), i++) { keys_sorted[i] = dk; } qsort(keys_sorted, n, sizeof(dns_dnsseckey_t *), keyalgtag_cmp); while (!ISC_LIST_EMPTY(keys_read)) { dns_dnsseckey_t *key = ISC_LIST_HEAD(keys_read); ISC_LIST_UNLINK(keys_read, key, link); } /* Save sorted list in 'keys' */ for (i = 0; i < n; i++) { ISC_LIST_APPEND(*keys, keys_sorted[i], link); } INSIST(ISC_LIST_EMPTY(keys_read)); isc_mem_cput(mctx, keys_sorted, n, sizeof(dns_dnsseckey_t *)); } 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); ksr->sigvalidity = dns_kasp_sigvalidity_dnskey(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); } static void cleanup(dns_dnsseckeylist_t *keys, dns_kasp_t *kasp) { while (!ISC_LIST_EMPTY(*keys)) { dns_dnsseckey_t *key = ISC_LIST_HEAD(*keys); ISC_LIST_UNLINK(*keys, key, link); dst_key_free(&key->key); dns_dnsseckey_destroy(mctx, &key); } dns_kasp_detach(&kasp); isc_buffer_t *cbuf = ISC_LIST_HEAD(cleanup_list); while (cbuf != NULL) { isc_buffer_t *nbuf = ISC_LIST_NEXT(cbuf, link); ISC_LIST_UNLINK(cleanup_list, cbuf, link); isc_buffer_free(&cbuf); cbuf = nbuf; } } static void progress(int p) { char c = '*'; switch (p) { case 0: c = '.'; break; case 1: c = '+'; break; case 2: c = '*'; break; case 3: c = ' '; break; default: break; } (void)putc(c, stderr); (void)fflush(stderr); } static void freerrset(dns_rdataset_t *rdataset) { dns_rdatalist_t *rdlist; dns_rdata_t *rdata; if (!dns_rdataset_isassociated(rdataset)) { return; } dns_rdatalist_fromrdataset(rdataset, &rdlist); for (rdata = ISC_LIST_HEAD(rdlist->rdata); rdata != NULL; rdata = ISC_LIST_HEAD(rdlist->rdata)) { ISC_LIST_UNLINK(rdlist->rdata, rdata, link); isc_mem_put(mctx, rdata, sizeof(*rdata)); } isc_mem_put(mctx, rdlist, sizeof(*rdlist)); dns_rdataset_disassociate(rdataset); } static void 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; char algstr[DNS_SECALG_FORMATSIZE]; char filename[PATH_MAX + 1]; char timestr[26]; /* Minimal buf as per ctime_r() spec. */ dst_key_t *key = NULL; int options = (DST_TYPE_PRIVATE | DST_TYPE_PUBLIC | DST_TYPE_STATE); 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)) { fatal("unsupported algorithm: %s", algstr); } INSIST(ksr->size >= 0); switch (ksr->alg) { case DST_ALG_RSASHA1: case DST_ALG_NSEC3RSASHA1: if (isc_fips_mode()) { /* verify-only in FIPS mode */ fatal("unsupported algorithm: %s", algstr); } FALLTHROUGH; case DST_ALG_RSASHA256: case DST_ALG_RSASHA512: if (ksr->size != 0 && (ksr->size < min_rsa || ksr->size > MAX_RSA)) { fatal("RSA key size %d out of range", ksr->size); } break; case DST_ALG_ECDSA256: ksr->size = 256; break; case DST_ALG_ECDSA384: ksr->size = 384; break; case DST_ALG_ED25519: ksr->size = 256; break; case DST_ALG_ED448: ksr->size = 456; break; default: show_progress = false; break; } isc_buffer_init(&buf, filename, sizeof(filename) - 1); /* Check existing keys. */ for (dns_dnsseckey_t *dk = ISC_LIST_HEAD(*keys); dk != NULL; dk = ISC_LIST_NEXT(dk, link)) { isc_stdtime_t act = 0, inact = 0; if (!dns_kasp_key_match(kaspkey, dk)) { continue; } (void)dst_key_gettime(dk->key, DST_TIME_ACTIVATE, &act); (void)dst_key_gettime(dk->key, DST_TIME_INACTIVE, &inact); /* * If this key's activation time is set after the inception * time, it is not eligble for the current bundle. */ if (act > inception) { continue; } /* * If this key's inactive time is set before the inception * time, it is not eligble for the current bundle. */ if (inact > 0 && inception >= inact) { continue; } /* Found matching existing key. */ if (verbose > 0 && show_progress) { fprintf(stderr, "Selecting key pair for bundle %s: ", timestr); fflush(stderr); } key = dk->key; *expiration = inact; goto output; } /* No existing keys match. */ do { conflict = false; if (verbose > 0 && show_progress) { fprintf(stderr, "Generating key pair for bundle %s: ", timestr); } if (ksr->keystore != NULL && ksr->policy != NULL) { ret = dns_keystore_keygen( ksr->keystore, name, ksr->policy, dns_rdataclass_in, mctx, ksr->alg, ksr->size, flags, &key); } else if (show_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, flags, DNS_KEYPROTO_DNSSEC, dns_rdataclass_in, NULL, mctx, &key, NULL); } if (ret != ISC_R_SUCCESS) { fatal("failed to generate key %s/%s: %s\n", namestr, algstr, isc_result_totext(ret)); } /* Do not overwrite an existing key. */ if (key_collision(key, name, ksr->keydir, mctx, dns_kasp_key_tagmin(kaspkey), dns_kasp_key_tagmax(kaspkey), NULL)) { conflict = true; if (verbose > 0) { isc_buffer_clear(&buf); ret = dst_key_buildfilename(key, 0, ksr->keydir, &buf); if (ret == ISC_R_SUCCESS) { fprintf(stderr, "%s: %s already exists, or " "might collide with another " "key upon revokation. " "Generating a new key\n", program, filename); } } dst_key_free(&key); } } while (conflict); freekey = true; /* Set key timing metadata. */ 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, 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; 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; } else { *expiration = 0; } ret = dst_key_tofile(key, options, ksr->keydir); if (ret != ISC_R_SUCCESS) { char keystr[DST_KEY_FORMATSIZE]; dst_key_format(key, keystr, sizeof(keystr)); fatal("failed to write key %s: %s\n", keystr, isc_result_totext(ret)); } output: isc_buffer_clear(&buf); ret = dst_key_buildfilename(key, 0, NULL, &buf); if (ret != ISC_R_SUCCESS) { fatal("dst_key_buildfilename returned: %s\n", isc_result_totext(ret)); } printf("%s\n", filename); fflush(stdout); if (freekey) { dst_key_free(&key); } } static void print_rdata(dns_rdataset_t *rrset) { isc_buffer_t target; isc_region_t r; isc_result_t ret; char buf[4096]; isc_buffer_init(&target, buf, sizeof(buf)); ret = dns_rdataset_totext(rrset, name, false, false, &target); if (ret != ISC_R_SUCCESS) { fatal("failed to print rdata"); } isc_buffer_usedregion(&target, &r); fprintf(stdout, "%.*s", (int)r.length, (char *)r.base); } static isc_stdtime_t print_dnskeys(dns_kasp_key_t *kaspkey, dns_ttl_t ttl, dns_dnsseckeylist_t *keys, isc_stdtime_t inception, isc_stdtime_t next_inception) { char algstr[DNS_SECALG_FORMATSIZE]; char timestr[26]; /* Minimal buf as per ctime_r() spec. */ dns_rdatalist_t *rdatalist = NULL; dns_rdataset_t rdataset = DNS_RDATASET_INIT; isc_result_t ret = ISC_R_SUCCESS; isc_stdtime_t next_bundle = next_inception; isc_stdtime_tostring(inception, timestr, sizeof(timestr)); dns_secalg_format(dns_kasp_key_algorithm(kaspkey), algstr, sizeof(algstr)); /* Fetch matching key pair. */ rdatalist = isc_mem_get(mctx, sizeof(*rdatalist)); dns_rdatalist_init(rdatalist); rdatalist->rdclass = dns_rdataclass_in; rdatalist->type = dns_rdatatype_dnskey; rdatalist->ttl = ttl; for (dns_dnsseckey_t *dk = ISC_LIST_HEAD(*keys); dk != NULL; dk = ISC_LIST_NEXT(dk, link)) { isc_stdtime_t pub = 0, del = 0; (void)dst_key_gettime(dk->key, DST_TIME_PUBLISH, &pub); (void)dst_key_gettime(dk->key, DST_TIME_DELETE, &del); /* Determine next bundle. */ if (pub > 0 && pub > inception && pub < next_bundle) { next_bundle = pub; } if (del > 0 && del > inception && del < next_bundle) { next_bundle = del; } /* Find matching key. */ if (!dns_kasp_key_match(kaspkey, dk)) { continue; } if (pub > inception) { continue; } if (del != 0 && inception >= del) { continue; } /* Found matching key pair, add DNSKEY record to RRset. */ isc_buffer_t buf; isc_buffer_t *newbuf = NULL; dns_rdata_t *rdata = NULL; isc_region_t r; unsigned char rdatabuf[DST_KEY_MAXSIZE]; rdata = isc_mem_get(mctx, sizeof(*rdata)); dns_rdata_init(rdata); isc_buffer_init(&buf, rdatabuf, sizeof(rdatabuf)); 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(rdatalist->rdata, rdata, link); ISC_LIST_APPEND(cleanup_list, newbuf, link); isc_buffer_clear(newbuf); } /* Error if no key pair found. */ if (ISC_LIST_EMPTY(rdatalist->rdata)) { fatal("no %s/%s zsk key pair found for bundle %s", namestr, algstr, timestr); } /* All good, print DNSKEY RRset. */ dns_rdatalist_tordataset(rdatalist, &rdataset); print_rdata(&rdataset); fail: /* Cleanup */ freerrset(&rdataset); if (ret != ISC_R_SUCCESS) { fatal("failed to print %s/%s zsk key pair found for bundle %s", namestr, algstr, timestr); } return next_bundle; } 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); /* Bundle header */ if (rrset->type == dns_rdatatype_dnskey) { char timestr[26]; /* Minimal buf as per ctime_r() spec. */ char utc[sizeof("YYYYMMDDHHSSMM")]; isc_buffer_t timebuf; isc_buffer_t b; isc_region_t r; isc_buffer_init(&timebuf, timestr, sizeof(timestr)); isc_stdtime_tostring(inception, timestr, sizeof(timestr)); isc_buffer_init(&b, utc, sizeof(utc)); ret = dns_time32_totext(inception, &b); if (ret != ISC_R_SUCCESS) { fatal("failed to convert bundle time32 to text: %s", isc_result_totext(ret)); } isc_buffer_usedregion(&b, &r); fprintf(stdout, ";; SignedKeyResponse 1.0 %.*s (%s)\n", (int)r.length, r.base, timestr); } /* RRset */ print_rdata(rrset); /* Signatures */ rrsiglist = isc_mem_get(mctx, sizeof(*rrsiglist)); dns_rdatalist_init(rrsiglist); rrsiglist->rdclass = dns_rdataclass_in; rrsiglist->type = dns_rdatatype_rrsig; rrsiglist->ttl = rrset->ttl; for (dns_dnsseckey_t *dk = ISC_LIST_HEAD(*keys); dk != NULL; dk = ISC_LIST_NEXT(dk, link)) { isc_buffer_t buf; isc_buffer_t *newbuf = NULL; dns_rdata_t rdata = DNS_RDATA_INIT; dns_rdata_t *rrsig = NULL; isc_region_t rs; 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)); ret = dns_dnssec_sign(name, rrset, dk->key, &clockskew, &expiration, mctx, &buf, &rdata); if (ret != ISC_R_SUCCESS) { fatal("failed to sign KSR"); } isc_buffer_usedregion(&buf, &rs); isc_buffer_allocate(mctx, &newbuf, rs.length); isc_buffer_putmem(newbuf, rs.base, rs.length); isc_buffer_usedregion(newbuf, &rs); dns_rdata_fromregion(rrsig, dns_rdataclass_in, dns_rdatatype_rrsig, &rs); ISC_LIST_APPEND(rrsiglist->rdata, rrsig, link); ISC_LIST_APPEND(cleanup_list, newbuf, link); isc_buffer_clear(newbuf); } 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 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; isc_stdtime_t next_bundle = next_inception; dns_rdatalist_init(dnskeylist); dnskeylist->rdclass = dns_rdataclass_in; dnskeylist->type = dns_rdatatype_dnskey; dnskeylist->ttl = ksr->ttl; dns_rdatalist_init(cdnskeylist); cdnskeylist->rdclass = dns_rdataclass_in; cdnskeylist->type = dns_rdatatype_cdnskey; cdnskeylist->ttl = ksr->ttl; dns_rdatalist_init(cdslist); cdslist->rdclass = dns_rdataclass_in; cdslist->type = dns_rdatatype_cds; cdslist->ttl = ksr->ttl; 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 */ (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); 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; rdata = isc_mem_get(mctx, sizeof(*rdata)); dns_rdata_init(rdata); isc_buffer_init(&buf, cdnskeybuf, sizeof(cdnskeybuf)); 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_cdnskey, &r); if (dns_kasp_cdnskey(kasp)) { ISC_LIST_APPEND(cdnskeylist->rdata, rdata, link); } ISC_LIST_APPEND(cleanup_list, newbuf, link); isc_buffer_clear(newbuf); /* CDS */ for (dns_kasp_digest_t *alg = ISC_LIST_HEAD(digests); alg != NULL; alg = ISC_LIST_NEXT(alg, link)) { isc_buffer_t *newbuf2 = NULL; dns_rdata_t *rdata2 = NULL; dns_rdata_t cds = DNS_RDATA_INIT; rdata2 = isc_mem_get(mctx, sizeof(*rdata2)); dns_rdata_init(rdata2); CHECK(dns_ds_buildrdata(name, rdata, alg->digest, cdsbuf, &cds)); cds.type = dns_rdatatype_cds; dns_rdata_toregion(&cds, &rcds); isc_buffer_allocate(mctx, &newbuf2, rcds.length); isc_buffer_putmem(newbuf2, rcds.base, rcds.length); isc_buffer_usedregion(newbuf2, &rcds); dns_rdata_fromregion(rdata2, dns_rdataclass_in, dns_rdatatype_cds, &rcds); ISC_LIST_APPEND(cdslist->rdata, rdata2, link); ISC_LIST_APPEND(cleanup_list, newbuf2, link); isc_buffer_clear(newbuf2); } if (!dns_kasp_cdnskey(kasp)) { isc_mem_put(mctx, rdata, sizeof(*rdata)); } } /* All good */ dns_rdatalist_tordataset(dnskeylist, dnskeyset); dns_rdatalist_tordataset(cdnskeylist, cdnskeyset); dns_rdatalist_tordataset(cdslist, cdsset); return next_bundle; fail: fatal("failed to create KSK/CDS/CDNSKEY"); return 0; } static void 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) { 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); while (inception <= next_inception) { 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; } 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(&zsk); } static isc_result_t parse_dnskey(isc_lex_t *lex, char *owner, isc_buffer_t *buf, dns_ttl_t *ttl) { dns_fixedname_t dfname; dns_name_t *dname = NULL; dns_rdataclass_t rdclass = dns_rdataclass_in; isc_buffer_t b; isc_result_t ret; isc_token_t token; unsigned int opt = ISC_LEXOPT_EOL; isc_lex_setcomments(lex, ISC_LEXCOMMENT_DNSMASTERFILE); /* Read the domain name */ if (!strcmp(owner, "@")) { BADTOKEN(); } dname = dns_fixedname_initname(&dfname); isc_buffer_init(&b, owner, strlen(owner)); isc_buffer_add(&b, strlen(owner)); ret = dns_name_fromtext(dname, &b, dns_rootname, 0, NULL); if (ret != ISC_R_SUCCESS) { return ret; } if (dns_name_compare(dname, name) != 0) { return DNS_R_BADOWNERNAME; } isc_buffer_clear(&b); /* Read the next word: either TTL, class, or type */ NEXTTOKEN(lex, opt, &token); if (token.type != isc_tokentype_string) { BADTOKEN(); } /* If it's a TTL, read the next one */ ret = dns_ttl_fromtext(&token.value.as_textregion, ttl); if (ret == ISC_R_SUCCESS) { NEXTTOKEN(lex, opt, &token); } if (token.type != isc_tokentype_string) { BADTOKEN(); } /* If it's a class, read the next one */ ret = dns_rdataclass_fromtext(&rdclass, &token.value.as_textregion); if (ret == ISC_R_SUCCESS) { NEXTTOKEN(lex, opt, &token); } if (token.type != isc_tokentype_string) { BADTOKEN(); } /* Must be the type */ if (strcasecmp(STR(token), "DNSKEY") != 0) { BADTOKEN(); } ret = dns_rdata_fromtext(NULL, rdclass, dns_rdatatype_dnskey, lex, name, 0, mctx, buf, NULL); cleanup: isc_lex_setcomments(lex, 0); return ret; } static void keygen(ksr_ctx_t *ksr) { dns_kasp_t *kasp = NULL; dns_dnsseckeylist_t keys; bool noop = true; /* Check parameters */ checkparams(ksr, "keygen"); /* Get the policy */ getkasp(ksr, &kasp); /* Get existing keys */ get_dnskeys(ksr, &keys); /* Set context */ setcontext(ksr, kasp); /* Key generation */ 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) && !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); ksr->keystore = dns_kasp_key_keystore(kk); ksr->size = dns_kasp_key_size(kk); noop = false; for (isc_stdtime_t inception = ksr->start, act = ksr->start; inception < ksr->end; inception += ksr->lifetime) { create_key(ksr, kasp, kk, &keys, inception, act, &act); if (ksr->lifetime == 0) { /* unlimited lifetime, but not infinite loop */ break; } } } if (noop) { fatal("no keys created for policy '%s'", ksr->policy); } /* Cleanup */ cleanup(&keys, kasp); } static void request(ksr_ctx_t *ksr) { char timestr[26]; /* Minimal buf as per ctime_r() spec. */ dns_dnsseckeylist_t keys; dns_kasp_t *kasp = NULL; isc_stdtime_t next = 0; isc_stdtime_t inception = 0; /* Check parameters */ checkparams(ksr, "request"); /* Get the policy */ getkasp(ksr, &kasp); /* Get keys */ get_dnskeys(ksr, &keys); /* Set context */ setcontext(ksr, kasp); /* Create request */ inception = ksr->start; while (inception <= ksr->end) { char utc[sizeof("YYYYMMDDHHSSMM")]; isc_buffer_t b; isc_region_t r; isc_result_t ret; isc_stdtime_tostring(inception, timestr, sizeof(timestr)); isc_buffer_init(&b, utc, sizeof(utc)); ret = dns_time32_totext(inception, &b); if (ret != ISC_R_SUCCESS) { fatal("failed to convert bundle time32 to text: %s", isc_result_totext(ret)); } isc_buffer_usedregion(&b, &r); fprintf(stdout, ";; KeySigningRequest 1.0 %.*s (%s)\n", (int)r.length, r.base, timestr); next = ksr->end + 1; for (dns_kasp_key_t *kk = ISC_LIST_HEAD(dns_kasp_keys(kasp)); kk != NULL; kk = ISC_LIST_NEXT(kk, link)) { /* * Output the DNSKEY records for the current bundle * that starts at 'inception. The 'next' variable is * updated to the start time of the * next bundle, determined by the earliest publication * or withdrawal of a key that is after the current * inception. */ if (dns_kasp_key_ksk(kk)) { /* We only want ZSKs in the request. */ continue; } next = print_dnskeys(kk, ksr->ttl, &keys, inception, next); } inception = next; } isc_stdtime_tostring(ksr->now, timestr, sizeof(timestr)); fprintf(stdout, ";; KeySigningRequest 1.0 generated at %s by %s\n", timestr, PACKAGE_VERSION); /* Cleanup */ cleanup(&keys, kasp); } static void sign(ksr_ctx_t *ksr) { char timestr[26]; /* Minimal buf as per ctime_r() spec. */ bool have_bundle = false; dns_dnsseckeylist_t keys; dns_kasp_t *kasp = NULL; dns_rdatalist_t *rdatalist = NULL; isc_result_t ret; isc_stdtime_t inception; isc_lex_t *lex = NULL; isc_lexspecials_t specials; isc_token_t token; unsigned int opt = ISC_LEXOPT_EOL; /* Check parameters */ checkparams(ksr, "sign"); if (ksr->file == NULL) { fatal("'sign' requires a KSR file"); } /* Get the policy */ getkasp(ksr, &kasp); /* Get keys */ get_dnskeys(ksr, &keys); /* Set context */ setcontext(ksr, kasp); /* Sign request */ inception = ksr->start; isc_lex_create(mctx, KSR_LINESIZE, &lex); memset(specials, 0, sizeof(specials)); specials['('] = 1; specials[')'] = 1; specials['"'] = 1; isc_lex_setspecials(lex, specials); ret = isc_lex_openfile(lex, ksr->file); if (ret != ISC_R_SUCCESS) { fatal("unable to open KSR file %s: %s", ksr->file, isc_result_totext(ret)); } for (ret = isc_lex_gettoken(lex, opt, &token); ret == ISC_R_SUCCESS; ret = isc_lex_gettoken(lex, opt, &token)) { if (token.type != isc_tokentype_string) { fatal("bad KSR file %s(%lu): syntax error", ksr->file, isc_lex_getsourceline(lex)); } if (strcmp(STR(token), ";;") == 0) { char bundle[KSR_LINESIZE]; isc_stdtime_t next_inception; CHECK(isc_lex_gettoken(lex, opt, &token)); if (token.type != isc_tokentype_string || strcmp(STR(token), "KeySigningRequest") != 0) { fatal("bad KSR file %s(%lu): expected " "'KeySigningRequest'", ksr->file, isc_lex_getsourceline(lex)); } CHECK(isc_lex_gettoken(lex, opt, &token)); if (token.type != isc_tokentype_string) { fatal("bad KSR file %s(%lu): expected string", ksr->file, isc_lex_getsourceline(lex)); } if (strcmp(STR(token), "1.0") != 0) { fatal("bad KSR file %s(%lu): expected version", ksr->file, isc_lex_getsourceline(lex)); } CHECK(isc_lex_gettoken(lex, opt, &token)); if (token.type != isc_tokentype_string) { fatal("bad KSR file %s(%lu): expected datetime", ksr->file, isc_lex_getsourceline(lex)); } if (strcmp(STR(token), "generated") == 0) { /* Final bundle */ goto readline; } /* Date and time of bundle */ sscanf(STR(token), "%s", bundle); next_inception = strtotime(bundle, ksr->now, ksr->now, NULL); if (have_bundle) { /* Sign previous bundle */ sign_bundle(ksr, kasp, inception, next_inception, rdatalist, &keys); fprintf(stdout, "\n"); } /* Start next bundle */ rdatalist = isc_mem_get(mctx, sizeof(*rdatalist)); dns_rdatalist_init(rdatalist); rdatalist->rdclass = dns_rdataclass_in; rdatalist->type = dns_rdatatype_dnskey; rdatalist->ttl = ksr->ttl; inception = next_inception; have_bundle = true; readline: /* Read remainder of header line */ do { ret = isc_lex_gettoken(lex, opt, &token); if (ret != ISC_R_SUCCESS) { fatal("bad KSR file %s(%lu): bad " "header (%s)", ksr->file, isc_lex_getsourceline(lex), isc_result_totext(ret)); } } while (token.type != isc_tokentype_eol); } else { /* Parse DNSKEY */ dns_ttl_t ttl = ksr->ttl; isc_buffer_t buf; isc_buffer_t *newbuf = NULL; dns_rdata_t *rdata = NULL; isc_region_t r; u_char rdatabuf[DST_KEY_MAXSIZE]; INSIST(rdatalist != NULL); rdata = isc_mem_get(mctx, sizeof(*rdata)); dns_rdata_init(rdata); isc_buffer_init(&buf, rdatabuf, sizeof(rdatabuf)); ret = parse_dnskey(lex, STR(token), &buf, &ttl); if (ret != ISC_R_SUCCESS) { fatal("bad KSR file %s(%lu): bad DNSKEY (%s)", ksr->file, isc_lex_getsourceline(lex), isc_result_totext(ret)); } 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); if (rdatalist != NULL && ttl < rdatalist->ttl) { rdatalist->ttl = ttl; } ISC_LIST_APPEND(rdatalist->rdata, rdata, link); ISC_LIST_APPEND(cleanup_list, newbuf, link); isc_buffer_clear(newbuf); } } if (ret != ISC_R_EOF) { fatal("bad KSR file %s(%lu): trailing garbage data", ksr->file, isc_lex_getsourceline(lex)); } /* Final bundle */ if (have_bundle && rdatalist != NULL) { sign_bundle(ksr, kasp, inception, ksr->end, rdatalist, &keys); } else { fatal("bad KSR file %s(%lu): no bundles", ksr->file, isc_lex_getsourceline(lex)); } /* Bundle footer */ isc_stdtime_tostring(ksr->now, timestr, sizeof(timestr)); fprintf(stdout, ";; SignedKeyResponse 1.0 generated at %s by %s\n", timestr, PACKAGE_VERSION); fail: isc_lex_destroy(&lex); cleanup(&keys, kasp); } int main(int argc, char *argv[]) { isc_result_t ret; isc_buffer_t buf; int ch; char *endp; bool set_fips_mode = false; #if OPENSSL_VERSION_NUMBER >= 0x30000000L && OPENSSL_API_LEVEL >= 30000 OSSL_PROVIDER *fips = NULL, *base = NULL; #endif ksr_ctx_t ksr = { .now = isc_stdtime_now(), }; isc_mem_create(&mctx); isc_commandline_errprint = false; #define OPTIONS "E:e:Ff:hi:K:k:l:ov:V" while ((ch = isc_commandline_parse(argc, argv, OPTIONS)) != -1) { switch (ch) { case 'E': engine = isc_commandline_argument; break; case 'e': ksr.end = strtotime(isc_commandline_argument, ksr.now, ksr.now, &ksr.setend); break; case 'F': set_fips_mode = true; break; case 'f': ksr.file = isc_commandline_argument; break; case 'h': usage(0); break; case 'i': ksr.start = strtotime(isc_commandline_argument, ksr.now, ksr.now, &ksr.setstart); break; case 'K': ksr.keydir = isc_commandline_argument; ret = try_dir(ksr.keydir); if (ret != ISC_R_SUCCESS) { fatal("cannot open directory %s: %s", ksr.keydir, isc_result_totext(ret)); } break; case 'k': ksr.policy = isc_commandline_argument; break; case 'l': ksr.configfile = isc_commandline_argument; break; case 'o': ksr.ksk = true; break; case 'V': version(program); break; case 'v': verbose = strtoul(isc_commandline_argument, &endp, 0); if (*endp != '\0') { fatal("-v must be followed by a number"); } break; default: usage(1); break; } } argv += isc_commandline_index; argc -= isc_commandline_index; if (argc != 2) { fatal("must provide a command and zone name"); } ret = dst_lib_init(mctx, engine); if (ret != ISC_R_SUCCESS) { fatal("could not initialize dst: %s", isc_result_totext(ret)); } /* * After dst_lib_init which will set FIPS mode if requested * at build time. The minumums are both raised to 2048. */ if (isc_fips_mode()) { min_rsa = min_dh = 2048; } setup_logging(mctx, &lctx); if (set_fips_mode) { #if OPENSSL_VERSION_NUMBER >= 0x30000000L && OPENSSL_API_LEVEL >= 30000 fips = OSSL_PROVIDER_load(NULL, "fips"); if (fips == NULL) { fatal("Failed to load FIPS provider"); } base = OSSL_PROVIDER_load(NULL, "base"); if (base == NULL) { OSSL_PROVIDER_unload(fips); fatal("Failed to load base provider"); } #endif if (!isc_fips_mode()) { if (isc_fips_set_mode(1) != ISC_R_SUCCESS) { fatal("setting FIPS mode failed"); } } } /* zone */ namestr = argv[1]; name = dns_fixedname_initname(&fname); isc_buffer_init(&buf, argv[1], strlen(argv[1])); isc_buffer_add(&buf, strlen(argv[1])); ret = dns_name_fromtext(name, &buf, dns_rootname, 0, NULL); if (ret != ISC_R_SUCCESS) { fatal("invalid zone name %s: %s", argv[1], isc_result_totext(ret)); } /* command */ if (strcmp(argv[0], "keygen") == 0) { keygen(&ksr); } else if (strcmp(argv[0], "request") == 0) { request(&ksr); } else if (strcmp(argv[0], "sign") == 0) { sign(&ksr); } else { fatal("unknown command '%s'", argv[0]); } exit(0); }