1/*	$NetBSD: dnssec-dsfromkey.c,v 1.11 2024/02/21 22:51:02 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#include <inttypes.h>
19#include <stdbool.h>
20#include <stdlib.h>
21
22#include <isc/attributes.h>
23#include <isc/buffer.h>
24#include <isc/commandline.h>
25#include <isc/dir.h>
26#include <isc/hash.h>
27#include <isc/mem.h>
28#include <isc/print.h>
29#include <isc/result.h>
30#include <isc/string.h>
31#include <isc/util.h>
32
33#include <dns/callbacks.h>
34#include <dns/db.h>
35#include <dns/dbiterator.h>
36#include <dns/ds.h>
37#include <dns/fixedname.h>
38#include <dns/keyvalues.h>
39#include <dns/log.h>
40#include <dns/master.h>
41#include <dns/name.h>
42#include <dns/rdata.h>
43#include <dns/rdataclass.h>
44#include <dns/rdataset.h>
45#include <dns/rdatasetiter.h>
46#include <dns/rdatatype.h>
47
48#include <dst/dst.h>
49
50#include "dnssectool.h"
51
52const char *program = "dnssec-dsfromkey";
53
54static dns_rdataclass_t rdclass;
55static dns_fixedname_t fixed;
56static dns_name_t *name = NULL;
57static isc_mem_t *mctx = NULL;
58static uint32_t ttl;
59static bool emitttl = false;
60
61static isc_result_t
62initname(char *setname) {
63	isc_result_t result;
64	isc_buffer_t buf;
65
66	name = dns_fixedname_initname(&fixed);
67
68	isc_buffer_init(&buf, setname, strlen(setname));
69	isc_buffer_add(&buf, strlen(setname));
70	result = dns_name_fromtext(name, &buf, dns_rootname, 0, NULL);
71	return (result);
72}
73
74static void
75db_load_from_stream(dns_db_t *db, FILE *fp) {
76	isc_result_t result;
77	dns_rdatacallbacks_t callbacks;
78
79	dns_rdatacallbacks_init(&callbacks);
80	result = dns_db_beginload(db, &callbacks);
81	if (result != ISC_R_SUCCESS) {
82		fatal("dns_db_beginload failed: %s", isc_result_totext(result));
83	}
84
85	result = dns_master_loadstream(fp, name, name, rdclass, 0, &callbacks,
86				       mctx);
87	if (result != ISC_R_SUCCESS) {
88		fatal("can't load from input: %s", isc_result_totext(result));
89	}
90
91	result = dns_db_endload(db, &callbacks);
92	if (result != ISC_R_SUCCESS) {
93		fatal("dns_db_endload failed: %s", isc_result_totext(result));
94	}
95}
96
97static isc_result_t
98loadset(const char *filename, dns_rdataset_t *rdataset) {
99	isc_result_t result;
100	dns_db_t *db = NULL;
101	dns_dbnode_t *node = NULL;
102	char setname[DNS_NAME_FORMATSIZE];
103
104	dns_name_format(name, setname, sizeof(setname));
105
106	result = dns_db_create(mctx, "rbt", name, dns_dbtype_zone, rdclass, 0,
107			       NULL, &db);
108	if (result != ISC_R_SUCCESS) {
109		fatal("can't create database");
110	}
111
112	if (strcmp(filename, "-") == 0) {
113		db_load_from_stream(db, stdin);
114		filename = "input";
115	} else {
116		result = dns_db_load(db, filename, dns_masterformat_text, 0);
117		if (result != ISC_R_SUCCESS && result != DNS_R_SEENINCLUDE) {
118			fatal("can't load %s: %s", filename,
119			      isc_result_totext(result));
120		}
121	}
122
123	result = dns_db_findnode(db, name, false, &node);
124	if (result != ISC_R_SUCCESS) {
125		fatal("can't find %s node in %s", setname, filename);
126	}
127
128	result = dns_db_findrdataset(db, node, NULL, dns_rdatatype_dnskey, 0, 0,
129				     rdataset, NULL);
130
131	if (result == ISC_R_NOTFOUND) {
132		fatal("no DNSKEY RR for %s in %s", setname, filename);
133	} else if (result != ISC_R_SUCCESS) {
134		fatal("dns_db_findrdataset");
135	}
136
137	if (node != NULL) {
138		dns_db_detachnode(db, &node);
139	}
140	if (db != NULL) {
141		dns_db_detach(&db);
142	}
143	return (result);
144}
145
146static isc_result_t
147loadkeyset(char *dirname, dns_rdataset_t *rdataset) {
148	isc_result_t result;
149	char filename[PATH_MAX + 1];
150	isc_buffer_t buf;
151
152	dns_rdataset_init(rdataset);
153
154	isc_buffer_init(&buf, filename, sizeof(filename));
155	if (dirname != NULL) {
156		/* allow room for a trailing slash */
157		if (strlen(dirname) >= isc_buffer_availablelength(&buf)) {
158			return (ISC_R_NOSPACE);
159		}
160		isc_buffer_putstr(&buf, dirname);
161		if (dirname[strlen(dirname) - 1] != '/') {
162			isc_buffer_putstr(&buf, "/");
163		}
164	}
165
166	if (isc_buffer_availablelength(&buf) < 7) {
167		return (ISC_R_NOSPACE);
168	}
169	isc_buffer_putstr(&buf, "keyset-");
170
171	result = dns_name_tofilenametext(name, false, &buf);
172	check_result(result, "dns_name_tofilenametext()");
173	if (isc_buffer_availablelength(&buf) == 0) {
174		return (ISC_R_NOSPACE);
175	}
176	isc_buffer_putuint8(&buf, 0);
177
178	return (loadset(filename, rdataset));
179}
180
181static void
182loadkey(char *filename, unsigned char *key_buf, unsigned int key_buf_size,
183	dns_rdata_t *rdata) {
184	isc_result_t result;
185	dst_key_t *key = NULL;
186	isc_buffer_t keyb;
187	isc_region_t r;
188
189	dns_rdata_init(rdata);
190
191	isc_buffer_init(&keyb, key_buf, key_buf_size);
192
193	result = dst_key_fromnamedfile(filename, NULL, DST_TYPE_PUBLIC, mctx,
194				       &key);
195	if (result != ISC_R_SUCCESS) {
196		fatal("can't load %s.key: %s", filename,
197		      isc_result_totext(result));
198	}
199
200	if (verbose > 2) {
201		char keystr[DST_KEY_FORMATSIZE];
202
203		dst_key_format(key, keystr, sizeof(keystr));
204		fprintf(stderr, "%s: %s\n", program, keystr);
205	}
206
207	result = dst_key_todns(key, &keyb);
208	if (result != ISC_R_SUCCESS) {
209		fatal("can't decode key");
210	}
211
212	isc_buffer_usedregion(&keyb, &r);
213	dns_rdata_fromregion(rdata, dst_key_class(key), dns_rdatatype_dnskey,
214			     &r);
215
216	rdclass = dst_key_class(key);
217
218	name = dns_fixedname_initname(&fixed);
219	dns_name_copy(dst_key_name(key), name);
220
221	dst_key_free(&key);
222}
223
224static void
225logkey(dns_rdata_t *rdata) {
226	isc_result_t result;
227	dst_key_t *key = NULL;
228	isc_buffer_t buf;
229	char keystr[DST_KEY_FORMATSIZE];
230
231	isc_buffer_init(&buf, rdata->data, rdata->length);
232	isc_buffer_add(&buf, rdata->length);
233	result = dst_key_fromdns(name, rdclass, &buf, mctx, &key);
234	if (result != ISC_R_SUCCESS) {
235		return;
236	}
237
238	dst_key_format(key, keystr, sizeof(keystr));
239	fprintf(stderr, "%s: %s\n", program, keystr);
240
241	dst_key_free(&key);
242}
243
244static void
245emit(dns_dsdigest_t dt, bool showall, bool cds, dns_rdata_t *rdata) {
246	isc_result_t result;
247	unsigned char buf[DNS_DS_BUFFERSIZE];
248	char text_buf[DST_KEY_MAXTEXTSIZE];
249	char name_buf[DNS_NAME_MAXWIRE];
250	char class_buf[10];
251	isc_buffer_t textb, nameb, classb;
252	isc_region_t r;
253	dns_rdata_t ds;
254	dns_rdata_dnskey_t dnskey;
255
256	isc_buffer_init(&textb, text_buf, sizeof(text_buf));
257	isc_buffer_init(&nameb, name_buf, sizeof(name_buf));
258	isc_buffer_init(&classb, class_buf, sizeof(class_buf));
259
260	dns_rdata_init(&ds);
261
262	result = dns_rdata_tostruct(rdata, &dnskey, NULL);
263	if (result != ISC_R_SUCCESS) {
264		fatal("can't convert DNSKEY");
265	}
266
267	if ((dnskey.flags & DNS_KEYFLAG_REVOKE) != 0) {
268		return;
269	}
270
271	if ((dnskey.flags & DNS_KEYFLAG_KSK) == 0 && !showall) {
272		return;
273	}
274
275	result = dns_ds_buildrdata(name, rdata, dt, buf, &ds);
276	if (result != ISC_R_SUCCESS) {
277		fatal("can't build record");
278	}
279
280	result = dns_name_totext(name, false, &nameb);
281	if (result != ISC_R_SUCCESS) {
282		fatal("can't print name");
283	}
284
285	result = dns_rdata_tofmttext(&ds, (dns_name_t *)NULL, 0, 0, 0, "",
286				     &textb);
287
288	if (result != ISC_R_SUCCESS) {
289		fatal("can't print rdata");
290	}
291
292	result = dns_rdataclass_totext(rdclass, &classb);
293	if (result != ISC_R_SUCCESS) {
294		fatal("can't print class");
295	}
296
297	isc_buffer_usedregion(&nameb, &r);
298	printf("%.*s ", (int)r.length, r.base);
299
300	if (emitttl) {
301		printf("%u ", ttl);
302	}
303
304	isc_buffer_usedregion(&classb, &r);
305	printf("%.*s", (int)r.length, r.base);
306
307	if (cds) {
308		printf(" CDS ");
309	} else {
310		printf(" DS ");
311	}
312
313	isc_buffer_usedregion(&textb, &r);
314	printf("%.*s\n", (int)r.length, r.base);
315}
316
317static void
318emits(bool showall, bool cds, dns_rdata_t *rdata) {
319	unsigned i, n;
320
321	n = sizeof(dtype) / sizeof(dtype[0]);
322	for (i = 0; i < n; i++) {
323		if (dtype[i] != 0) {
324			emit(dtype[i], showall, cds, rdata);
325		}
326	}
327}
328
329noreturn static void
330usage(void);
331
332static void
333usage(void) {
334	fprintf(stderr, "Usage:\n");
335	fprintf(stderr, "    %s [options] keyfile\n\n", program);
336	fprintf(stderr, "    %s [options] -f zonefile [zonename]\n\n", program);
337	fprintf(stderr, "    %s [options] -s dnsname\n\n", program);
338	fprintf(stderr, "    %s [-h|-V]\n\n", program);
339	fprintf(stderr, "Version: %s\n", PACKAGE_VERSION);
340	fprintf(stderr, "Options:\n"
341			"    -1: digest algorithm SHA-1\n"
342			"    -2: digest algorithm SHA-256\n"
343			"    -a algorithm: digest algorithm (SHA-1, SHA-256 or "
344			"SHA-384)\n"
345			"    -A: include all keys in DS set, not just KSKs (-f "
346			"only)\n"
347			"    -c class: rdata class for DS set (default IN) (-f "
348			"or -s only)\n"
349			"    -C: print CDS records\n"
350			"    -f zonefile: read keys from a zone file\n"
351			"    -h: print help information\n"
352			"    -K directory: where to find key or keyset files\n"
353			"    -s: read keys from keyset-<dnsname> file\n"
354			"    -T: TTL of output records (omitted by default)\n"
355			"    -v level: verbosity\n"
356			"    -V: print version information\n");
357	fprintf(stderr, "Output: DS or CDS RRs\n");
358
359	exit(-1);
360}
361
362int
363main(int argc, char **argv) {
364	char *classname = NULL;
365	char *filename = NULL, *dir = NULL, *namestr;
366	char *endp, *arg1;
367	int ch;
368	bool cds = false;
369	bool usekeyset = false;
370	bool showall = false;
371	isc_result_t result;
372	isc_log_t *log = NULL;
373	dns_rdataset_t rdataset;
374	dns_rdata_t rdata;
375
376	dns_rdata_init(&rdata);
377
378	if (argc == 1) {
379		usage();
380	}
381
382	isc_mem_create(&mctx);
383
384	isc_commandline_errprint = false;
385
386#define OPTIONS "12Aa:Cc:d:Ff:K:l:sT:v:hV"
387	while ((ch = isc_commandline_parse(argc, argv, OPTIONS)) != -1) {
388		switch (ch) {
389		case '1':
390			add_dtype(DNS_DSDIGEST_SHA1);
391			break;
392		case '2':
393			add_dtype(DNS_DSDIGEST_SHA256);
394			break;
395		case 'A':
396			showall = true;
397			break;
398		case 'a':
399			add_dtype(strtodsdigest(isc_commandline_argument));
400			break;
401		case 'C':
402			cds = true;
403			break;
404		case 'c':
405			classname = isc_commandline_argument;
406			break;
407		case 'd':
408			fprintf(stderr,
409				"%s: the -d option is deprecated; "
410				"use -K\n",
411				program);
412		/* fall through */
413		case 'K':
414			dir = isc_commandline_argument;
415			if (strlen(dir) == 0U) {
416				fatal("directory must be non-empty string");
417			}
418			break;
419		case 'f':
420			filename = isc_commandline_argument;
421			break;
422		case 'l':
423			fatal("-l option (DLV lookaside) is obsolete");
424			break;
425		case 's':
426			usekeyset = true;
427			break;
428		case 'T':
429			emitttl = true;
430			ttl = strtottl(isc_commandline_argument);
431			break;
432		case 'v':
433			verbose = strtol(isc_commandline_argument, &endp, 0);
434			if (*endp != '\0') {
435				fatal("-v must be followed by a number");
436			}
437			break;
438		case 'F':
439			/* Reserved for FIPS mode */
440			FALLTHROUGH;
441		case '?':
442			if (isc_commandline_option != '?') {
443				fprintf(stderr, "%s: invalid argument -%c\n",
444					program, isc_commandline_option);
445			}
446			FALLTHROUGH;
447		case 'h':
448			/* Does not return. */
449			usage();
450
451		case 'V':
452			/* Does not return. */
453			version(program);
454
455		default:
456			fprintf(stderr, "%s: unhandled option -%c\n", program,
457				isc_commandline_option);
458			exit(1);
459		}
460	}
461
462	rdclass = strtoclass(classname);
463
464	if (usekeyset && filename != NULL) {
465		fatal("cannot use both -s and -f");
466	}
467
468	/* When not using -f, -A is implicit */
469	if (filename == NULL) {
470		showall = true;
471	}
472
473	/* Default digest type if none specified. */
474	if (dtype[0] == 0) {
475		dtype[0] = DNS_DSDIGEST_SHA256;
476	}
477
478	/*
479	 * Use local variable arg1 so that clang can correctly analyse
480	 * reachable paths rather than 'argc < isc_commandline_index + 1'.
481	 */
482	arg1 = argv[isc_commandline_index];
483	if (arg1 == NULL && filename == NULL) {
484		fatal("the key file name was not specified");
485	}
486	if (arg1 != NULL && argv[isc_commandline_index + 1] != NULL) {
487		fatal("extraneous arguments");
488	}
489
490	result = dst_lib_init(mctx, NULL);
491	if (result != ISC_R_SUCCESS) {
492		fatal("could not initialize dst: %s",
493		      isc_result_totext(result));
494	}
495
496	setup_logging(mctx, &log);
497
498	dns_rdataset_init(&rdataset);
499
500	if (usekeyset || filename != NULL) {
501		if (arg1 == NULL) {
502			/* using file name as the zone name */
503			namestr = filename;
504		} else {
505			namestr = arg1;
506		}
507
508		result = initname(namestr);
509		if (result != ISC_R_SUCCESS) {
510			fatal("could not initialize name %s", namestr);
511		}
512
513		if (usekeyset) {
514			result = loadkeyset(dir, &rdataset);
515		} else {
516			INSIST(filename != NULL);
517			result = loadset(filename, &rdataset);
518		}
519
520		if (result != ISC_R_SUCCESS) {
521			fatal("could not load DNSKEY set: %s\n",
522			      isc_result_totext(result));
523		}
524
525		for (result = dns_rdataset_first(&rdataset);
526		     result == ISC_R_SUCCESS;
527		     result = dns_rdataset_next(&rdataset))
528		{
529			dns_rdata_init(&rdata);
530			dns_rdataset_current(&rdataset, &rdata);
531
532			if (verbose > 2) {
533				logkey(&rdata);
534			}
535
536			emits(showall, cds, &rdata);
537		}
538	} else {
539		unsigned char key_buf[DST_KEY_MAXSIZE];
540
541		loadkey(arg1, key_buf, DST_KEY_MAXSIZE, &rdata);
542
543		emits(showall, cds, &rdata);
544	}
545
546	if (dns_rdataset_isassociated(&rdataset)) {
547		dns_rdataset_disassociate(&rdataset);
548	}
549	cleanup_logging(&log);
550	dst_lib_destroy();
551	if (verbose > 10) {
552		isc_mem_stats(mctx, stdout);
553	}
554	isc_mem_destroy(&mctx);
555
556	fflush(stdout);
557	if (ferror(stdout)) {
558		fprintf(stderr, "write error\n");
559		return (1);
560	} else {
561		return (0);
562	}
563}
564