1 /* $NetBSD: dnssectool.c,v 1.11 2025/01/26 16:24:33 christos Exp $ */ 2 3 /* 4 * Copyright (C) Internet Systems Consortium, Inc. ("ISC") 5 * 6 * SPDX-License-Identifier: MPL-2.0 7 * 8 * This Source Code Form is subject to the terms of the Mozilla Public 9 * License, v. 2.0. If a copy of the MPL was not distributed with this 10 * file, you can obtain one at https://mozilla.org/MPL/2.0/. 11 * 12 * See the COPYRIGHT file distributed with this work for additional 13 * information regarding copyright ownership. 14 */ 15 16 /*! \file */ 17 18 /*% 19 * DNSSEC Support Routines. 20 */ 21 22 #include <inttypes.h> 23 #include <stdbool.h> 24 #include <stdlib.h> 25 #include <unistd.h> 26 27 #include <isc/base32.h> 28 #include <isc/buffer.h> 29 #include <isc/commandline.h> 30 #include <isc/dir.h> 31 #include <isc/file.h> 32 #include <isc/heap.h> 33 #include <isc/list.h> 34 #include <isc/mem.h> 35 #include <isc/result.h> 36 #include <isc/string.h> 37 #include <isc/time.h> 38 #include <isc/tls.h> 39 #include <isc/tm.h> 40 #include <isc/util.h> 41 42 #include <dns/db.h> 43 #include <dns/dbiterator.h> 44 #include <dns/dnssec.h> 45 #include <dns/fixedname.h> 46 #include <dns/journal.h> 47 #include <dns/keyvalues.h> 48 #include <dns/log.h> 49 #include <dns/name.h> 50 #include <dns/nsec.h> 51 #include <dns/nsec3.h> 52 #include <dns/rdataclass.h> 53 #include <dns/rdataset.h> 54 #include <dns/rdatasetiter.h> 55 #include <dns/rdatastruct.h> 56 #include <dns/rdatatype.h> 57 #include <dns/secalg.h> 58 #include <dns/time.h> 59 60 #include "dnssectool.h" 61 62 #define KEYSTATES_NVALUES 4 63 static const char *keystates[KEYSTATES_NVALUES] = { 64 "hidden", 65 "rumoured", 66 "omnipresent", 67 "unretentive", 68 }; 69 70 int verbose = 0; 71 bool quiet = false; 72 const char *journal = NULL; 73 dns_dsdigest_t dtype[8]; 74 75 static fatalcallback_t *fatalcallback = NULL; 76 77 void 78 fatal(const char *format, ...) { 79 va_list args; 80 81 fprintf(stderr, "%s: fatal: ", program); 82 va_start(args, format); 83 vfprintf(stderr, format, args); 84 va_end(args); 85 fprintf(stderr, "\n"); 86 if (fatalcallback != NULL) { 87 (*fatalcallback)(); 88 } 89 _exit(EXIT_FAILURE); 90 } 91 92 void 93 setfatalcallback(fatalcallback_t *callback) { 94 fatalcallback = callback; 95 } 96 97 void 98 check_result(isc_result_t result, const char *message) { 99 if (result != ISC_R_SUCCESS) { 100 fatal("%s: %s", message, isc_result_totext(result)); 101 } 102 } 103 104 void 105 vbprintf(int level, const char *fmt, ...) { 106 va_list ap; 107 if (level > verbose) { 108 return; 109 } 110 va_start(ap, fmt); 111 fprintf(stderr, "%s: ", program); 112 vfprintf(stderr, fmt, ap); 113 va_end(ap); 114 } 115 116 void 117 version(const char *name) { 118 printf("%s %s\n", name, PACKAGE_VERSION); 119 exit(EXIT_SUCCESS); 120 } 121 122 void 123 sig_format(dns_rdata_rrsig_t *sig, char *cp, unsigned int size) { 124 char namestr[DNS_NAME_FORMATSIZE]; 125 char algstr[DNS_NAME_FORMATSIZE]; 126 127 dns_name_format(&sig->signer, namestr, sizeof(namestr)); 128 dns_secalg_format(sig->algorithm, algstr, sizeof(algstr)); 129 snprintf(cp, size, "%s/%s/%d", namestr, algstr, sig->keyid); 130 } 131 132 void 133 setup_logging(isc_mem_t *mctx, isc_log_t **logp) { 134 isc_logdestination_t destination; 135 isc_logconfig_t *logconfig = NULL; 136 isc_log_t *log = NULL; 137 int level; 138 139 if (verbose < 0) { 140 verbose = 0; 141 } 142 switch (verbose) { 143 case 0: 144 /* 145 * We want to see warnings about things like out-of-zone 146 * data in the master file even when not verbose. 147 */ 148 level = ISC_LOG_WARNING; 149 break; 150 case 1: 151 level = ISC_LOG_INFO; 152 break; 153 default: 154 level = ISC_LOG_DEBUG(verbose - 2 + 1); 155 break; 156 } 157 158 isc_log_create(mctx, &log, &logconfig); 159 isc_log_setcontext(log); 160 dns_log_init(log); 161 dns_log_setcontext(log); 162 isc_log_settag(logconfig, program); 163 164 /* 165 * Set up a channel similar to default_stderr except: 166 * - the logging level is passed in 167 * - the program name and logging level are printed 168 * - no time stamp is printed 169 */ 170 destination.file.stream = stderr; 171 destination.file.name = NULL; 172 destination.file.versions = ISC_LOG_ROLLNEVER; 173 destination.file.maximum_size = 0; 174 isc_log_createchannel(logconfig, "stderr", ISC_LOG_TOFILEDESC, level, 175 &destination, 176 ISC_LOG_PRINTTAG | ISC_LOG_PRINTLEVEL); 177 178 RUNTIME_CHECK(isc_log_usechannel(logconfig, "stderr", NULL, NULL) == 179 ISC_R_SUCCESS); 180 181 *logp = log; 182 } 183 184 void 185 cleanup_logging(isc_log_t **logp) { 186 isc_log_t *log; 187 188 REQUIRE(logp != NULL); 189 190 log = *logp; 191 *logp = NULL; 192 193 if (log == NULL) { 194 return; 195 } 196 197 isc_log_destroy(&log); 198 isc_log_setcontext(NULL); 199 dns_log_setcontext(NULL); 200 } 201 202 static isc_stdtime_t 203 time_units(isc_stdtime_t offset, char *suffix, const char *str) { 204 switch (suffix[0]) { 205 case 'Y': 206 case 'y': 207 return offset * (365 * 24 * 3600); 208 case 'M': 209 case 'm': 210 switch (suffix[1]) { 211 case 'O': 212 case 'o': 213 return offset * (30 * 24 * 3600); 214 case 'I': 215 case 'i': 216 return offset * 60; 217 case '\0': 218 fatal("'%s' ambiguous: use 'mi' for minutes " 219 "or 'mo' for months", 220 str); 221 default: 222 fatal("time value %s is invalid", str); 223 } 224 UNREACHABLE(); 225 break; 226 case 'W': 227 case 'w': 228 return offset * (7 * 24 * 3600); 229 case 'D': 230 case 'd': 231 return offset * (24 * 3600); 232 case 'H': 233 case 'h': 234 return offset * 3600; 235 case 'S': 236 case 's': 237 case '\0': 238 return offset; 239 default: 240 fatal("time value %s is invalid", str); 241 } 242 UNREACHABLE(); 243 return 0; /* silence compiler warning */ 244 } 245 246 static bool 247 isnone(const char *str) { 248 return (strcasecmp(str, "none") == 0) || 249 (strcasecmp(str, "never") == 0) || 250 (strcasecmp(str, "unset") == 0); 251 } 252 253 dns_ttl_t 254 strtottl(const char *str) { 255 const char *orig = str; 256 dns_ttl_t ttl; 257 char *endp; 258 259 if (isnone(str)) { 260 return (dns_ttl_t)0; 261 } 262 263 ttl = strtol(str, &endp, 0); 264 if (ttl == 0 && endp == str) { 265 fatal("TTL must be numeric"); 266 } 267 ttl = time_units(ttl, endp, orig); 268 return ttl; 269 } 270 271 dst_key_state_t 272 strtokeystate(const char *str) { 273 if (isnone(str)) { 274 return DST_KEY_STATE_NA; 275 } 276 277 for (int i = 0; i < KEYSTATES_NVALUES; i++) { 278 if (keystates[i] != NULL && strcasecmp(str, keystates[i]) == 0) 279 { 280 return (dst_key_state_t)i; 281 } 282 } 283 fatal("unknown key state %s", str); 284 } 285 286 isc_stdtime_t 287 strtotime(const char *str, int64_t now, int64_t base, bool *setp) { 288 int64_t val, offset; 289 isc_result_t result; 290 const char *orig = str; 291 char *endp; 292 size_t n; 293 struct tm tm; 294 295 if (isnone(str)) { 296 SET_IF_NOT_NULL(setp, false); 297 return (isc_stdtime_t)0; 298 } 299 300 SET_IF_NOT_NULL(setp, true); 301 302 if ((str[0] == '0' || str[0] == '-') && str[1] == '\0') { 303 return (isc_stdtime_t)0; 304 } 305 306 /* 307 * We accept times in the following formats: 308 * now([+-]offset) 309 * YYYYMMDD([+-]offset) 310 * YYYYMMDDhhmmss([+-]offset) 311 * Day Mon DD HH:MM:SS YYYY([+-]offset) 312 * 1234567890([+-]offset) 313 * [+-]offset 314 */ 315 n = strspn(str, "0123456789"); 316 if ((n == 8u || n == 14u) && 317 (str[n] == '\0' || str[n] == '-' || str[n] == '+')) 318 { 319 char timestr[15]; 320 321 strlcpy(timestr, str, sizeof(timestr)); 322 timestr[n] = 0; 323 if (n == 8u) { 324 strlcat(timestr, "000000", sizeof(timestr)); 325 } 326 result = dns_time64_fromtext(timestr, &val); 327 if (result != ISC_R_SUCCESS) { 328 fatal("time value %s is invalid: %s", orig, 329 isc_result_totext(result)); 330 } 331 base = val; 332 str += n; 333 } else if (n == 10u && 334 (str[n] == '\0' || str[n] == '-' || str[n] == '+')) 335 { 336 base = strtoll(str, &endp, 0); 337 str += 10; 338 } else if (strncmp(str, "now", 3) == 0) { 339 base = now; 340 str += 3; 341 } else if (str[0] >= 'A' && str[0] <= 'Z') { 342 /* parse ctime() format as written by `dnssec-settime -p` */ 343 endp = isc_tm_strptime(str, "%a %b %d %H:%M:%S %Y", &tm); 344 if (endp != str + 24) { 345 fatal("time value %s is invalid", orig); 346 } 347 base = mktime(&tm); 348 str += 24; 349 } 350 351 if (str[0] == '\0') { 352 return (isc_stdtime_t)base; 353 } else if (str[0] == '+') { 354 offset = strtol(str + 1, &endp, 0); 355 offset = time_units((isc_stdtime_t)offset, endp, orig); 356 val = base + offset; 357 } else if (str[0] == '-') { 358 offset = strtol(str + 1, &endp, 0); 359 offset = time_units((isc_stdtime_t)offset, endp, orig); 360 val = base - offset; 361 } else { 362 fatal("time value %s is invalid", orig); 363 } 364 365 return (isc_stdtime_t)val; 366 } 367 368 dns_rdataclass_t 369 strtoclass(const char *str) { 370 isc_textregion_t r; 371 dns_rdataclass_t rdclass; 372 isc_result_t result; 373 374 if (str == NULL) { 375 return dns_rdataclass_in; 376 } 377 r.base = UNCONST(str); 378 r.length = strlen(str); 379 result = dns_rdataclass_fromtext(&rdclass, &r); 380 if (result != ISC_R_SUCCESS) { 381 fatal("unknown class %s", str); 382 } 383 return rdclass; 384 } 385 386 unsigned int 387 strtodsdigest(const char *str) { 388 isc_textregion_t r; 389 dns_dsdigest_t alg; 390 isc_result_t result; 391 392 r.base = UNCONST(str); 393 r.length = strlen(str); 394 result = dns_dsdigest_fromtext(&alg, &r); 395 if (result != ISC_R_SUCCESS) { 396 fatal("unknown DS algorithm %s", str); 397 } 398 return alg; 399 } 400 401 static int 402 cmp_dtype(const void *ap, const void *bp) { 403 int a = *(const uint8_t *)ap; 404 int b = *(const uint8_t *)bp; 405 return a - b; 406 } 407 408 void 409 add_dtype(unsigned int dt) { 410 unsigned int i, n; 411 412 /* ensure there is space for a zero terminator */ 413 n = sizeof(dtype) / sizeof(dtype[0]) - 1; 414 for (i = 0; i < n; i++) { 415 if (dtype[i] == dt) { 416 return; 417 } 418 if (dtype[i] == 0) { 419 dtype[i] = dt; 420 qsort(dtype, i + 1, 1, cmp_dtype); 421 return; 422 } 423 } 424 fatal("too many -a digest type arguments"); 425 } 426 427 isc_result_t 428 try_dir(const char *dirname) { 429 isc_result_t result; 430 isc_dir_t d; 431 432 isc_dir_init(&d); 433 result = isc_dir_open(&d, dirname); 434 if (result == ISC_R_SUCCESS) { 435 isc_dir_close(&d); 436 } 437 return result; 438 } 439 440 /* 441 * Check private key version compatibility. 442 */ 443 void 444 check_keyversion(dst_key_t *key, char *keystr) { 445 int major, minor; 446 dst_key_getprivateformat(key, &major, &minor); 447 INSIST(major <= DST_MAJOR_VERSION); /* invalid private key */ 448 449 if (major < DST_MAJOR_VERSION || minor < DST_MINOR_VERSION) { 450 fatal("Key %s has incompatible format version %d.%d, " 451 "use -f to force upgrade to new version.", 452 keystr, major, minor); 453 } 454 if (minor > DST_MINOR_VERSION) { 455 fatal("Key %s has incompatible format version %d.%d, " 456 "use -f to force downgrade to current version.", 457 keystr, major, minor); 458 } 459 } 460 461 void 462 set_keyversion(dst_key_t *key) { 463 int major, minor; 464 dst_key_getprivateformat(key, &major, &minor); 465 INSIST(major <= DST_MAJOR_VERSION); 466 467 if (major != DST_MAJOR_VERSION || minor != DST_MINOR_VERSION) { 468 dst_key_setprivateformat(key, DST_MAJOR_VERSION, 469 DST_MINOR_VERSION); 470 } 471 472 /* 473 * If the key is from a version older than 1.3, set 474 * set the creation date 475 */ 476 if (major < 1 || (major == 1 && minor <= 2)) { 477 isc_stdtime_t now = isc_stdtime_now(); 478 dst_key_settime(key, DST_TIME_CREATED, now); 479 } 480 } 481 482 bool 483 key_collision(dst_key_t *dstkey, dns_name_t *name, const char *dir, 484 isc_mem_t *mctx, uint16_t min, uint16_t max, bool *exact) { 485 isc_result_t result; 486 bool conflict = false; 487 dns_dnsseckeylist_t matchkeys; 488 dns_dnsseckey_t *key = NULL; 489 uint16_t id, oldid; 490 uint32_t rid, roldid; 491 dns_secalg_t alg; 492 isc_stdtime_t now = isc_stdtime_now(); 493 494 if (exact != NULL) { 495 *exact = false; 496 } 497 498 id = dst_key_id(dstkey); 499 rid = dst_key_rid(dstkey); 500 alg = dst_key_alg(dstkey); 501 502 if (min != max) { 503 if (id < min || id > max) { 504 fprintf(stderr, "Key ID %d outside of [%u..%u]\n", id, 505 min, max); 506 return true; 507 } 508 if (rid < min || rid > max) { 509 fprintf(stderr, 510 "Revoked Key ID %d (for tag %d) outside of " 511 "[%u..%u]\n", 512 rid, id, min, max); 513 return true; 514 } 515 } 516 517 ISC_LIST_INIT(matchkeys); 518 result = dns_dnssec_findmatchingkeys(name, NULL, dir, NULL, now, mctx, 519 &matchkeys); 520 if (result == ISC_R_NOTFOUND) { 521 return false; 522 } 523 524 while (!ISC_LIST_EMPTY(matchkeys) && !conflict) { 525 key = ISC_LIST_HEAD(matchkeys); 526 if (dst_key_alg(key->key) != alg) { 527 goto next; 528 } 529 530 oldid = dst_key_id(key->key); 531 roldid = dst_key_rid(key->key); 532 533 if (oldid == rid || roldid == id || id == oldid) { 534 conflict = true; 535 if (id != oldid) { 536 if (verbose > 1) { 537 fprintf(stderr, 538 "Key ID %d could " 539 "collide with %d\n", 540 id, oldid); 541 } 542 } else { 543 if (exact != NULL) { 544 *exact = true; 545 } 546 if (verbose > 1) { 547 fprintf(stderr, "Key ID %d exists\n", 548 id); 549 } 550 } 551 } 552 553 next: 554 ISC_LIST_UNLINK(matchkeys, key, link); 555 dns_dnsseckey_destroy(mctx, &key); 556 } 557 558 /* Finish freeing the list */ 559 while (!ISC_LIST_EMPTY(matchkeys)) { 560 key = ISC_LIST_HEAD(matchkeys); 561 ISC_LIST_UNLINK(matchkeys, key, link); 562 dns_dnsseckey_destroy(mctx, &key); 563 } 564 565 return conflict; 566 } 567 568 bool 569 isoptarg(const char *arg, char **argv, void (*usage)(void)) { 570 if (!strcasecmp(isc_commandline_argument, arg)) { 571 if (argv[isc_commandline_index] == NULL) { 572 fprintf(stderr, "%s: missing argument -%c %s\n", 573 program, isc_commandline_option, 574 isc_commandline_argument); 575 usage(); 576 } 577 isc_commandline_argument = argv[isc_commandline_index]; 578 /* skip to next argument */ 579 isc_commandline_index++; 580 return true; 581 } 582 return false; 583 } 584 585 void 586 loadjournal(isc_mem_t *mctx, dns_db_t *db, const char *file) { 587 dns_journal_t *jnl = NULL; 588 isc_result_t result; 589 590 result = dns_journal_open(mctx, file, DNS_JOURNAL_READ, &jnl); 591 if (result == ISC_R_NOTFOUND) { 592 fprintf(stderr, "%s: journal file %s not found\n", program, 593 file); 594 goto cleanup; 595 } else if (result != ISC_R_SUCCESS) { 596 fatal("unable to open journal %s: %s\n", file, 597 isc_result_totext(result)); 598 } 599 600 if (dns_journal_empty(jnl)) { 601 dns_journal_destroy(&jnl); 602 return; 603 } 604 605 result = dns_journal_rollforward(jnl, db, 0); 606 switch (result) { 607 case ISC_R_SUCCESS: 608 case DNS_R_UPTODATE: 609 break; 610 611 case ISC_R_NOTFOUND: 612 case ISC_R_RANGE: 613 fatal("journal %s out of sync with zone", file); 614 615 default: 616 fatal("journal %s: %s\n", file, isc_result_totext(result)); 617 } 618 619 cleanup: 620 dns_journal_destroy(&jnl); 621 } 622 623 void 624 kasp_from_conf(cfg_obj_t *config, isc_mem_t *mctx, isc_log_t *lctx, 625 const char *name, const char *keydir, const char *engine, 626 dns_kasp_t **kaspp) { 627 isc_result_t result = ISC_R_NOTFOUND; 628 const cfg_listelt_t *element; 629 const cfg_obj_t *kasps = NULL; 630 dns_kasp_t *kasp = NULL, *kasp_next; 631 dns_kasplist_t kasplist; 632 const cfg_obj_t *keystores = NULL; 633 dns_keystore_t *ks = NULL, *ks_next; 634 dns_keystorelist_t kslist; 635 636 ISC_LIST_INIT(kasplist); 637 ISC_LIST_INIT(kslist); 638 639 (void)cfg_map_get(config, "key-store", &keystores); 640 for (element = cfg_list_first(keystores); element != NULL; 641 element = cfg_list_next(element)) 642 { 643 cfg_obj_t *kconfig = cfg_listelt_value(element); 644 ks = NULL; 645 result = cfg_keystore_fromconfig(kconfig, mctx, lctx, engine, 646 &kslist, NULL); 647 if (result != ISC_R_SUCCESS) { 648 fatal("failed to configure key-store '%s': %s", 649 cfg_obj_asstring(cfg_tuple_get(kconfig, "name")), 650 isc_result_totext(result)); 651 } 652 } 653 /* Default key-directory key store. */ 654 ks = NULL; 655 (void)cfg_keystore_fromconfig(NULL, mctx, lctx, engine, &kslist, &ks); 656 INSIST(ks != NULL); 657 if (keydir != NULL) { 658 /* '-K keydir' takes priority */ 659 dns_keystore_setdirectory(ks, keydir); 660 } 661 dns_keystore_detach(&ks); 662 663 (void)cfg_map_get(config, "dnssec-policy", &kasps); 664 for (element = cfg_list_first(kasps); element != NULL; 665 element = cfg_list_next(element)) 666 { 667 cfg_obj_t *kconfig = cfg_listelt_value(element); 668 kasp = NULL; 669 if (strcmp(cfg_obj_asstring(cfg_tuple_get(kconfig, "name")), 670 name) != 0) 671 { 672 continue; 673 } 674 675 result = cfg_kasp_fromconfig(kconfig, NULL, true, mctx, lctx, 676 &kslist, &kasplist, &kasp); 677 if (result != ISC_R_SUCCESS) { 678 fatal("failed to configure dnssec-policy '%s': %s", 679 cfg_obj_asstring(cfg_tuple_get(kconfig, "name")), 680 isc_result_totext(result)); 681 } 682 INSIST(kasp != NULL); 683 dns_kasp_freeze(kasp); 684 break; 685 } 686 687 *kaspp = kasp; 688 689 /* 690 * Cleanup kasp list. 691 */ 692 for (kasp = ISC_LIST_HEAD(kasplist); kasp != NULL; kasp = kasp_next) { 693 kasp_next = ISC_LIST_NEXT(kasp, link); 694 ISC_LIST_UNLINK(kasplist, kasp, link); 695 dns_kasp_detach(&kasp); 696 } 697 698 /* 699 * Cleanup keystore list. 700 */ 701 for (ks = ISC_LIST_HEAD(kslist); ks != NULL; ks = ks_next) { 702 ks_next = ISC_LIST_NEXT(ks, link); 703 ISC_LIST_UNLINK(kslist, ks, link); 704 dns_keystore_detach(&ks); 705 } 706 } 707