]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Implement dnssec-ksr sign
authorMatthijs Mekking <matthijs@isc.org>
Thu, 22 Feb 2024 14:16:03 +0000 (15:16 +0100)
committerMatthijs Mekking <matthijs@isc.org>
Fri, 19 Apr 2024 08:41:04 +0000 (10:41 +0200)
Add code that can create a Signed Key Response (SKR) given a Key
Signing Request (KSR), a DNSSEC policy, a set of keys and an interval.

bin/dnssec/dnssec-ksr.c
bin/dnssec/dnssec-ksr.rst

index 84115f8097dc80f7e4eb58edd155b8260a1d3474..a5ebc28d654d0acd320982df6ebb0f3ebf87e859 100644 (file)
 
 /*! \file */
 
+#include <ctype.h>
 #include <stdio.h>
 
 #include <isc/buffer.h>
 #include <isc/commandline.h>
 #include <isc/fips.h>
+#include <isc/lex.h>
 #include <isc/mem.h>
 
+#include <dns/callbacks.h>
 #include <dns/dnssec.h>
 #include <dns/fixedname.h>
 #include <dns/keyvalues.h>
@@ -27,6 +30,7 @@
 #include <dns/rdatalist.h>
 #include <dns/rdataset.h>
 #include <dns/time.h>
+#include <dns/ttl.h>
 
 #include "dnssectool.h"
 
@@ -50,6 +54,7 @@ static dns_name_t *name = NULL;
 struct ksr_ctx {
        const char *policy;
        const char *configfile;
+       const char *file;
        const char *keydir;
        dns_keystore_t *keystore;
        isc_stdtime_t now;
@@ -65,6 +70,8 @@ struct ksr_ctx {
        time_t propagation;
        time_t publishsafety;
        time_t retiresafety;
+       time_t sigrefresh;
+       time_t sigvalidity;
        time_t signdelay;
        time_t ttlsig;
 };
@@ -77,6 +84,29 @@ typedef struct ksr_ctx ksr_ctx_t;
 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 TTL_MAX INT32_MAX
+#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) { \
@@ -91,20 +121,23 @@ usage(int ret) {
        fprintf(stderr, "Version: %s\n", PACKAGE_VERSION);
        fprintf(stderr, "\n");
        fprintf(stderr, "Options:\n");
-       fprintf(stderr, "    -e <date/offset>: end date\n");
        fprintf(stderr, "    -E <engine>: name of an OpenSSL engine to use\n");
+       fprintf(stderr, "    -e <date/offset>: end date\n");
        fprintf(stderr, "    -F: FIPS mode\n");
+       fprintf(stderr, "    -f: KSR file to sign\n");
        fprintf(stderr, "    -i <date/offset>: start date\n");
-       fprintf(stderr, "    -K <directory>: write keys into directory\n");
+       fprintf(stderr, "    -K <directory>: key directory\n");
        fprintf(stderr, "    -k <policy>: name of a DNSSEC policy\n");
        fprintf(stderr, "    -l <file>: file with dnssec-policy config\n");
        fprintf(stderr, "    -h: print usage and exit\n");
-       fprintf(stderr, "    -v <level>: set verbosity level\n");
        fprintf(stderr, "    -V: print version information\n");
+       fprintf(stderr, "    -v <level>: 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);
 }
 
@@ -208,6 +241,8 @@ setcontext(ksr_ctx_t *ksr, dns_kasp_t *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->ttlsig = dns_kasp_zonemaxttl(kasp, true);
@@ -247,6 +282,27 @@ progress(int p) {
        (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_zsk(ksr_ctx_t *ksr, dns_kasp_key_t *kaspkey, dns_dnsseckeylist_t *keys,
           isc_stdtime_t inception, isc_stdtime_t active,
@@ -547,6 +603,161 @@ fail:
        return (next_bundle);
 }
 
+static void
+sign_rrset(ksr_ctx_t *ksr, isc_stdtime_t inception, isc_stdtime_t expiration,
+          dns_rdataset_t *rrset, dns_dnsseckeylist_t *keys) {
+       char timestr[26]; /* Minimal buf as per ctime_r() spec. */
+       char utc[sizeof("YYYYMMDDHHSSMM")];
+       dns_rdatalist_t *rrsiglist = NULL;
+       dns_rdataset_t rrsigset = DNS_RDATASET_INIT;
+       isc_buffer_t timebuf;
+       isc_buffer_t b;
+       isc_region_t r;
+       isc_result_t ret;
+
+       UNUSED(ksr);
+
+       /* Bundle header */
+       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);
+
+       /* DNSKEY 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 = NULL;
+               dns_rdata_t *rrsig = NULL;
+               isc_region_t rs;
+               unsigned char rdatabuf[SIG_FORMATSIZE];
+               isc_stdtime_t clockskew = inception - 3600;
+
+               rdata = isc_mem_get(mctx, sizeof(*rdata));
+               rrsig = isc_mem_get(mctx, sizeof(*rrsig));
+               dns_rdata_init(rdata);
+               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_buffer_clear(newbuf);
+       }
+       dns_rdatalist_tordataset(rrsiglist, &rrsigset);
+       print_rdata(&rrsigset);
+       freerrset(&rrsigset);
+}
+
+static void
+sign_bundle(ksr_ctx_t *ksr, isc_stdtime_t inception,
+           isc_stdtime_t next_inception, dns_rdatalist_t *rdatalist,
+           dns_dnsseckeylist_t *keys) {
+       dns_rdataset_t rrset = DNS_RDATASET_INIT;
+       isc_stdtime_t expiration;
+
+       dns_rdataset_init(&rrset);
+       dns_rdatalist_tordataset(rdatalist, &rrset);
+       expiration = inception + ksr->sigvalidity;
+       while (inception <= next_inception) {
+               sign_rrset(ksr, inception, expiration, &rrset, keys);
+               inception = expiration - ksr->sigrefresh;
+               expiration = inception + ksr->sigvalidity;
+       }
+       freerrset(&rrset);
+}
+
+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;
@@ -653,6 +864,174 @@ request(ksr_ctx_t *ksr) {
        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), "generated") == 0) {
+                               /* Final bundle */
+                               goto readline;
+                       } else if (strcmp(STR(token), "1.0") != 0) {
+                               fatal("bad KSR file %s(%lu): expected version",
+                                     ksr->file, isc_lex_getsourceline(lex));
+                       }
+                       /* Date and time of bundle */
+                       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));
+                       }
+
+                       sscanf(STR(token), "%s", bundle);
+                       next_inception = strtotime(bundle, ksr->now, ksr->now,
+                                                  NULL);
+
+                       if (have_bundle) {
+                               /* Sign previous bundle */
+                               sign_bundle(ksr, 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 = TTL_MAX;
+                       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 = TTL_MAX;
+                       isc_buffer_t buf;
+                       isc_buffer_t *newbuf = NULL;
+                       dns_rdata_t *rdata = NULL;
+                       isc_region_t r;
+                       u_char rdatabuf[DST_KEY_MAXSIZE];
+
+                       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);
+               }
+       }
+
+       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, 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:
+       /* Clean up */
+       isc_lex_destroy(&lex);
+       cleanup(&keys, kasp);
+}
+
 int
 main(int argc, char *argv[]) {
        isc_result_t ret;
@@ -671,19 +1050,22 @@ main(int argc, char *argv[]) {
 
        isc_commandline_errprint = false;
 
-#define OPTIONS "E:e:Fhi:K:k:l:v:V"
+#define OPTIONS "E:e:Ff:hi:K:k:l:v: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 'E':
-                       engine = isc_commandline_argument;
-                       break;
                case 'F':
                        set_fips_mode = true;
                        break;
+               case 'f':
+                       ksr.file = isc_commandline_argument;
+                       break;
                case 'h':
                        usage(0);
                        break;
@@ -776,6 +1158,8 @@ main(int argc, char *argv[]) {
                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]);
        }
index 4fe138b64b7d19e7859c9996edfed5c0fa16d0dd..1e5b57e3beeead5507e4f1fe9a9916f3c999e7cc 100644 (file)
@@ -114,6 +114,11 @@ Commands
 
   Create a Key Signing Request (KSR), given a DNSSEC policy and an interval.
 
+.. option:: sign
+
+  Sign a Key Signing Request (KSR), given a DNSSEC policy and an interval,
+  creating a Signed Key Response (SKR).
+
 Exit Status
 ~~~~~~~~~~~
 
@@ -130,11 +135,21 @@ given a ``dnssec-policy`` named "mypolicy":
 
     dnssec-ksr -i now -e +1y -k mypolicy -l named.conf keygen example.com
 
-Creating a Key Signing Request for the same zone and period can be done with:
+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.
+
+Signing the KSR created above can be done with:
 
 ::
 
-    dnssec-ksr -i now -e +1y -k mypolicy -l named.conf request example.com
+    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``.
 
 See Also
 ~~~~~~~~