xref: /freebsd/usr.sbin/certctl/certctl.c (revision d5f55356a2fbf8222fb236fe509821e12f1ea456)
1 /*-
2  * Copyright (c) 2023-2025 Dag-Erling Smørgrav <des@FreeBSD.org>
3  *
4  * SPDX-License-Identifier: BSD-2-Clause
5  */
6 
7 #include <sys/sysctl.h>
8 #include <sys/stat.h>
9 #include <sys/tree.h>
10 
11 #include <dirent.h>
12 #include <err.h>
13 #include <errno.h>
14 #include <fcntl.h>
15 #include <fts.h>
16 #include <paths.h>
17 #include <stdbool.h>
18 #include <stdio.h>
19 #include <stdlib.h>
20 #include <string.h>
21 #include <unistd.h>
22 
23 #include <openssl/ssl.h>
24 
25 #define info(fmt, ...)							\
26 	do {								\
27 		if (verbose)						\
28 			fprintf(stderr, fmt "\n", ##__VA_ARGS__);	\
29 	} while (0)
30 
31 static char *
32 xasprintf(const char *fmt, ...)
33 {
34 	va_list ap;
35 	char *str;
36 	int ret;
37 
38 	va_start(ap, fmt);
39 	ret = vasprintf(&str, fmt, ap);
40 	va_end(ap);
41 	if (ret < 0 || str == NULL)
42 		err(1, NULL);
43 	return (str);
44 }
45 
46 static char *
47 xstrdup(const char *str)
48 {
49 	char *dup;
50 
51 	if ((dup = strdup(str)) == NULL)
52 		err(1, NULL);
53 	return (dup);
54 }
55 
56 static void usage(void);
57 
58 static bool dryrun;
59 static bool longnames;
60 static bool nobundle;
61 static bool unprivileged;
62 static bool verbose;
63 
64 static const char *localbase;
65 static const char *destdir;
66 static const char *distbase;
67 static const char *metalog;
68 
69 static const char *uname = "root";
70 static const char *gname = "wheel";
71 
72 static const char *const default_trusted_paths[] = {
73 	"/usr/share/certs/trusted",
74 	"%L/share/certs/trusted",
75 	"%L/share/certs",
76 	NULL
77 };
78 static char **trusted_paths;
79 
80 static const char *const default_untrusted_paths[] = {
81 	"/usr/share/certs/untrusted",
82 	"%L/share/certs/untrusted",
83 	NULL
84 };
85 static char **untrusted_paths;
86 
87 static char *trusted_dest;
88 static char *untrusted_dest;
89 static char *bundle_dest;
90 
91 #define SSL_PATH		"/etc/ssl"
92 #define TRUSTED_DIR		"certs"
93 #define TRUSTED_PATH		SSL_PATH "/" TRUSTED_DIR
94 #define UNTRUSTED_DIR		"untrusted"
95 #define UNTRUSTED_PATH		SSL_PATH "/" UNTRUSTED_DIR
96 #define LEGACY_DIR		"blacklisted"
97 #define LEGACY_PATH		SSL_PATH "/" LEGACY_DIR
98 #define BUNDLE_FILE		"cert.pem"
99 #define BUNDLE_PATH		SSL_PATH "/" BUNDLE_FILE
100 
101 static FILE *mlf;
102 
103 /*
104  * Remove duplicate and trailing slashes from a path.
105  */
106 static char *
107 normalize_path(const char *str)
108 {
109 	char *buf, *dst;
110 
111 	if ((buf = malloc(strlen(str) + 1)) == NULL)
112 		err(1, NULL);
113 	for (dst = buf; *str != '\0'; dst++) {
114 		if ((*dst = *str++) == '/') {
115 			while (*str == '/')
116 				str++;
117 			if (*str == '\0')
118 				break;
119 		}
120 	}
121 	*dst = '\0';
122 	return (buf);
123 }
124 
125 /*
126  * Split a colon-separated list into a NULL-terminated array.
127  */
128 static char **
129 split_paths(const char *str)
130 {
131 	char **paths;
132 	const char *p, *q;
133 	unsigned int i, n;
134 
135 	for (p = str, n = 1; *p; p++) {
136 		if (*p == ':')
137 			n++;
138 	}
139 	if ((paths = calloc(n + 1, sizeof(*paths))) == NULL)
140 		err(1, NULL);
141 	for (p = q = str, i = 0; i < n; i++, p = q + 1) {
142 		q = strchrnul(p, ':');
143 		if ((paths[i] = strndup(p, q - p)) == NULL)
144 			err(1, NULL);
145 	}
146 	return (paths);
147 }
148 
149 /*
150  * Expand %L into LOCALBASE and prefix DESTDIR and DISTBASE as needed.
151  */
152 static char *
153 expand_path(const char *template)
154 {
155 	if (template[0] == '%' && template[1] == 'L')
156 		return (xasprintf("%s%s%s", destdir, localbase, template + 2));
157 	return (xasprintf("%s%s%s", destdir, distbase, template));
158 }
159 
160 /*
161  * Expand an array of paths.
162  */
163 static char **
164 expand_paths(const char *const *templates)
165 {
166 	char **paths;
167 	unsigned int i, n;
168 
169 	for (n = 0; templates[n] != NULL; n++)
170 		continue;
171 	if ((paths = calloc(n + 1, sizeof(*paths))) == NULL)
172 		err(1, NULL);
173 	for (i = 0; i < n; i++)
174 		paths[i] = expand_path(templates[i]);
175 	return (paths);
176 }
177 
178 /*
179  * If destdir is a prefix of path, returns a pointer to the rest of path,
180  * otherwise returns path.
181  *
182  * Note that this intentionally does not strip distbase from the path!
183  * Unlike destdir, distbase is expected to be included in the metalog.
184  */
185 static const char *
186 unexpand_path(const char *path)
187 {
188 	const char *p = path;
189 	const char *q = destdir;
190 
191 	while (*p && *p == *q) {
192 		p++;
193 		q++;
194 	}
195 	return (*q == '\0' && *p == '/' ? p : path);
196 }
197 
198 /*
199  * X509 certificate in a rank-balanced tree.
200  */
201 struct cert {
202 	RB_ENTRY(cert) entry;
203 	unsigned long hash;
204 	char *name;
205 	X509 *x509;
206 	char *path;
207 };
208 
209 static void
210 free_cert(struct cert *cert)
211 {
212 	free(cert->name);
213 	X509_free(cert->x509);
214 	free(cert->path);
215 	free(cert);
216 }
217 
218 static int
219 certcmp(const struct cert *a, const struct cert *b)
220 {
221 	return (X509_cmp(a->x509, b->x509));
222 }
223 
224 RB_HEAD(cert_tree, cert);
225 static struct cert_tree trusted = RB_INITIALIZER(&trusted);
226 static struct cert_tree untrusted = RB_INITIALIZER(&untrusted);
227 RB_GENERATE_STATIC(cert_tree, cert, entry, certcmp);
228 
229 static void
230 free_certs(struct cert_tree *tree)
231 {
232 	struct cert *cert, *tmp;
233 
234 	RB_FOREACH_SAFE(cert, cert_tree, tree, tmp) {
235 		RB_REMOVE(cert_tree, tree, cert);
236 		free_cert(cert);
237 	}
238 }
239 
240 static struct cert *
241 find_cert(struct cert_tree *haystack, X509 *x509)
242 {
243 	struct cert needle = { .x509 = x509 };
244 
245 	return (RB_FIND(cert_tree, haystack, &needle));
246 }
247 
248 /*
249  * File containing a certificate in a rank-balanced tree sorted by
250  * certificate hash and disambiguating counter.  This is needed because
251  * the certificate hash function is prone to collisions, necessitating a
252  * counter to distinguish certificates that hash to the same value.
253  */
254 struct file {
255 	RB_ENTRY(file) entry;
256 	const struct cert *cert;
257 	unsigned int c;
258 };
259 
260 static int
261 filecmp(const struct file *a, const struct file *b)
262 {
263 	if (a->cert->hash > b->cert->hash)
264 		return (1);
265 	if (a->cert->hash < b->cert->hash)
266 		return (-1);
267 	return (a->c - b->c);
268 }
269 
270 RB_HEAD(file_tree, file);
271 RB_GENERATE_STATIC(file_tree, file, entry, filecmp);
272 
273 /*
274  * Lexicographical sort for scandir().
275  */
276 static int
277 lexisort(const struct dirent **d1, const struct dirent **d2)
278 {
279 	return (strcmp((*d1)->d_name, (*d2)->d_name));
280 }
281 
282 /*
283  * Read certificate(s) from a single file and insert them into a tree.
284  * Ignore certificates that already exist in the tree.  If exclude is not
285  * null, also ignore certificates that exist in exclude.
286  *
287  * Returns the number certificates added to the tree, or -1 on failure.
288  */
289 static int
290 read_cert(const char *path, struct cert_tree *tree, struct cert_tree *exclude)
291 {
292 	FILE *f;
293 	X509 *x509;
294 	X509_NAME *name;
295 	struct cert *cert;
296 	unsigned long hash;
297 	int len, ni, no;
298 
299 	if ((f = fopen(path, "r")) == NULL) {
300 		warn("%s", path);
301 		return (-1);
302 	}
303 	for (ni = no = 0;
304 	     (x509 = PEM_read_X509(f, NULL, NULL, NULL)) != NULL;
305 	     ni++) {
306 		hash = X509_subject_name_hash(x509);
307 		if (exclude && find_cert(exclude, x509)) {
308 			info("%08lx: excluded", hash);
309 			X509_free(x509);
310 			continue;
311 		}
312 		if (find_cert(tree, x509)) {
313 			info("%08lx: duplicate", hash);
314 			X509_free(x509);
315 			continue;
316 		}
317 		if ((cert = calloc(1, sizeof(*cert))) == NULL)
318 			err(1, NULL);
319 		cert->x509 = x509;
320 		name = X509_get_subject_name(x509);
321 		cert->hash = X509_NAME_hash_ex(name, NULL, NULL, NULL);
322 		len = X509_NAME_get_text_by_NID(name, NID_commonName,
323 		    NULL, 0);
324 		if (len > 0) {
325 			if ((cert->name = malloc(len + 1)) == NULL)
326 				err(1, NULL);
327 			X509_NAME_get_text_by_NID(name, NID_commonName,
328 			    cert->name, len + 1);
329 		} else {
330 			/* fallback for certificates without CN */
331 			cert->name = X509_NAME_oneline(name, NULL, 0);
332 		}
333 		cert->path = xstrdup(unexpand_path(path));
334 		if (RB_INSERT(cert_tree, tree, cert) != NULL)
335 			errx(1, "unexpected duplicate");
336 		info("%08lx: %s", cert->hash, cert->name);
337 		no++;
338 	}
339 	/*
340 	 * ni is the number of certificates we found in the file.
341 	 * no is the number of certificates that weren't already in our
342 	 * tree or on the exclusion list.
343 	 */
344 	if (ni == 0)
345 		warnx("%s: no valid certificates found", path);
346 	fclose(f);
347 	return (no);
348 }
349 
350 /*
351  * Load all certificates found in the specified path into a tree,
352  * optionally excluding those that already exist in a different tree.
353  *
354  * Returns the number of certificates added to the tree, or -1 on failure.
355  */
356 static int
357 read_certs(const char *path, struct cert_tree *tree, struct cert_tree *exclude)
358 {
359 	struct stat sb;
360 	char *paths[] = { (char *)(uintptr_t)path, NULL };
361 	FTS *fts;
362 	FTSENT *ent;
363 	int fts_options = FTS_LOGICAL | FTS_NOCHDIR;
364 	int ret, total = 0;
365 
366 	if (stat(path, &sb) != 0) {
367 		return (-1);
368 	} else if (!S_ISDIR(sb.st_mode)) {
369 		errno = ENOTDIR;
370 		return (-1);
371 	}
372 	if ((fts = fts_open(paths, fts_options, NULL)) == NULL)
373 		err(1, "fts_open()");
374 	while ((ent = fts_read(fts)) != NULL) {
375 		if (ent->fts_info != FTS_F) {
376 			if (ent->fts_info == FTS_ERR)
377 				warnc(ent->fts_errno, "fts_read()");
378 			continue;
379 		}
380 		info("found %s", ent->fts_path);
381 		ret = read_cert(ent->fts_path, tree, exclude);
382 		if (ret > 0)
383 			total += ret;
384 	}
385 	fts_close(fts);
386 	return (total);
387 }
388 
389 /*
390  * Save the contents of a cert tree to disk.
391  *
392  * Returns 0 on success and -1 on failure.
393  */
394 static int
395 write_certs(const char *dir, struct cert_tree *tree)
396 {
397 	struct file_tree files = RB_INITIALIZER(&files);
398 	struct cert *cert;
399 	struct file *file, *tmp;
400 	struct dirent **dents, **ent;
401 	char *path, *tmppath = NULL;
402 	FILE *f;
403 	mode_t mode = 0444;
404 	int cmp, d, fd, ndents, ret = 0;
405 
406 	/*
407 	 * Start by generating unambiguous file names for each certificate
408 	 * and storing them in lexicographical order
409 	 */
410 	RB_FOREACH(cert, cert_tree, tree) {
411 		if ((file = calloc(1, sizeof(*file))) == NULL)
412 			err(1, NULL);
413 		file->cert = cert;
414 		for (file->c = 0; file->c < INT_MAX; file->c++)
415 			if (RB_INSERT(file_tree, &files, file) == NULL)
416 				break;
417 		if (file->c == INT_MAX)
418 			errx(1, "unable to disambiguate %08lx", cert->hash);
419 		free(cert->path);
420 		cert->path = xasprintf("%08lx.%d", cert->hash, file->c);
421 	}
422 	/*
423 	 * Open and scan the directory.
424 	 */
425 	if ((d = open(dir, O_DIRECTORY | O_RDONLY)) < 0 ||
426 #ifdef BOOTSTRAPPING
427 	    (ndents = scandir(dir, &dents, NULL, lexisort))
428 #else
429 	    (ndents = fdscandir(d, &dents, NULL, lexisort))
430 #endif
431 	    < 0)
432 		err(1, "%s", dir);
433 	/*
434 	 * Iterate over the directory listing and the certificate listing
435 	 * in parallel.  If the directory listing gets ahead of the
436 	 * certificate listing, we need to write the current certificate
437 	 * and advance the certificate listing.  If the certificate
438 	 * listing is ahead of the directory listing, we need to delete
439 	 * the current file and advance the directory listing.  If they
440 	 * are neck and neck, we have a match and could in theory compare
441 	 * the two, but in practice it's faster to just replace the
442 	 * current file with the current certificate (and advance both).
443 	 */
444 	ent = dents;
445 	file = RB_MIN(file_tree, &files);
446 	for (;;) {
447 		if (ent < dents + ndents) {
448 			/* skip directories */
449 			if ((*ent)->d_type == DT_DIR) {
450 				free(*ent++);
451 				continue;
452 			}
453 			if (file != NULL) {
454 				/* compare current dirent to current cert */
455 				path = file->cert->path;
456 				cmp = strcmp((*ent)->d_name, path);
457 			} else {
458 				/* trailing files in directory */
459 				path = NULL;
460 				cmp = -1;
461 			}
462 		} else {
463 			if (file != NULL) {
464 				/* trailing certificates */
465 				path = file->cert->path;
466 				cmp = 1;
467 			} else {
468 				/* end of both lists */
469 				path = NULL;
470 				break;
471 			}
472 		}
473 		if (cmp < 0) {
474 			/* a file on disk with no matching certificate */
475 			info("removing %s/%s", dir, (*ent)->d_name);
476 			if (!dryrun)
477 				(void)unlinkat(d, (*ent)->d_name, 0);
478 			free(*ent++);
479 			continue;
480 		}
481 		if (cmp == 0) {
482 			/* a file on disk with a matching certificate */
483 			info("replacing %s/%s", dir, (*ent)->d_name);
484 			if (dryrun) {
485 				fd = open(_PATH_DEVNULL, O_WRONLY);
486 			} else {
487 				tmppath = xasprintf(".%s", path);
488 				fd = openat(d, tmppath,
489 				    O_CREAT | O_WRONLY | O_TRUNC, mode);
490 				if (!unprivileged && fd >= 0)
491 					(void)fchmod(fd, mode);
492 			}
493 			free(*ent++);
494 		} else {
495 			/* a certificate with no matching file */
496 			info("writing %s/%s", dir, path);
497 			if (dryrun) {
498 				fd = open(_PATH_DEVNULL, O_WRONLY);
499 			} else {
500 				tmppath = xasprintf(".%s", path);
501 				fd = openat(d, tmppath,
502 				    O_CREAT | O_WRONLY | O_EXCL, mode);
503 			}
504 		}
505 		/* write the certificate */
506 		if (fd < 0 ||
507 		    (f = fdopen(fd, "w")) == NULL ||
508 		    !PEM_write_X509(f, file->cert->x509)) {
509 			if (tmppath != NULL && fd >= 0) {
510 				int serrno = errno;
511 				(void)unlinkat(d, tmppath, 0);
512 				errno = serrno;
513 			}
514 			err(1, "%s/%s", dir, tmppath ? tmppath : path);
515 		}
516 		/* rename temp file if applicable */
517 		if (tmppath != NULL) {
518 			if (ret == 0 && renameat(d, tmppath, d, path) != 0) {
519 				warn("%s/%s", dir, path);
520 				ret = -1;
521 			}
522 			if (ret != 0)
523 				(void)unlinkat(d, tmppath, 0);
524 			free(tmppath);
525 			tmppath = NULL;
526 		}
527 		fflush(f);
528 		/* emit metalog */
529 		if (mlf != NULL) {
530 			fprintf(mlf, ".%s/%s type=file "
531 			    "uname=%s gname=%s mode=%#o size=%ld\n",
532 			    unexpand_path(dir), path,
533 			    uname, gname, mode, ftell(f));
534 		}
535 		fclose(f);
536 		/* advance certificate listing */
537 		tmp = RB_NEXT(file_tree, &files, file);
538 		RB_REMOVE(file_tree, &files, file);
539 		free(file);
540 		file = tmp;
541 	}
542 	free(dents);
543 	close(d);
544 	return (ret);
545 }
546 
547 /*
548  * Save all certs in a tree to a single file (bundle).
549  *
550  * Returns 0 on success and -1 on failure.
551  */
552 static int
553 write_bundle(const char *dir, const char *file, struct cert_tree *tree)
554 {
555 	struct cert *cert;
556 	char *tmpfile = NULL;
557 	FILE *f;
558 	int d, fd, ret = 0;
559 	mode_t mode = 0444;
560 
561 	if (dir != NULL) {
562 		if ((d = open(dir, O_DIRECTORY | O_RDONLY)) < 0)
563 			err(1, "%s", dir);
564 	} else {
565 		dir = ".";
566 		d = AT_FDCWD;
567 	}
568 	info("writing %s/%s", dir, file);
569 	if (dryrun) {
570 		fd = open(_PATH_DEVNULL, O_WRONLY);
571 	} else {
572 		tmpfile = xasprintf(".%s", file);
573 		fd = openat(d, tmpfile, O_WRONLY | O_CREAT | O_EXCL, mode);
574 	}
575 	if (fd < 0 || (f = fdopen(fd, "w")) == NULL) {
576 		if (tmpfile != NULL && fd >= 0) {
577 			int serrno = errno;
578 			(void)unlinkat(d, tmpfile, 0);
579 			errno = serrno;
580 		}
581 		err(1, "%s/%s", dir, tmpfile ? tmpfile : file);
582 	}
583 	RB_FOREACH(cert, cert_tree, tree) {
584 		if (!PEM_write_X509(f, cert->x509)) {
585 			warn("%s/%s", dir, tmpfile ? tmpfile : file);
586 			ret = -1;
587 			break;
588 		}
589 	}
590 	if (tmpfile != NULL) {
591 		if (ret == 0 && renameat(d, tmpfile, d, file) != 0) {
592 			warn("%s/%s", dir, file);
593 			ret = -1;
594 		}
595 		if (ret != 0)
596 			(void)unlinkat(d, tmpfile, 0);
597 		free(tmpfile);
598 	}
599 	if (ret == 0 && mlf != NULL) {
600 		fprintf(mlf,
601 		    ".%s/%s type=file uname=%s gname=%s mode=%#o size=%ld\n",
602 		    unexpand_path(dir), file, uname, gname, mode, ftell(f));
603 	}
604 	fclose(f);
605 	if (d != AT_FDCWD)
606 		close(d);
607 	return (ret);
608 }
609 
610 /*
611  * Load trusted certificates.
612  *
613  * Returns the number of certificates loaded.
614  */
615 static unsigned int
616 load_trusted(bool all, struct cert_tree *exclude)
617 {
618 	unsigned int i, n;
619 	int ret;
620 
621 	/* load external trusted certs */
622 	for (i = n = 0; all && trusted_paths[i] != NULL; i++) {
623 		ret = read_certs(trusted_paths[i], &trusted, exclude);
624 		if (ret > 0)
625 			n += ret;
626 	}
627 
628 	/* load installed trusted certs */
629 	ret = read_certs(trusted_dest, &trusted, exclude);
630 	if (ret > 0)
631 		n += ret;
632 
633 	info("%d trusted certificates found", n);
634 	return (n);
635 }
636 
637 /*
638  * Load untrusted certificates.
639  *
640  * Returns the number of certificates loaded.
641  */
642 static unsigned int
643 load_untrusted(bool all)
644 {
645 	char *path;
646 	unsigned int i, n;
647 	int ret;
648 
649 	/* load external untrusted certs */
650 	for (i = n = 0; all && untrusted_paths[i] != NULL; i++) {
651 		ret = read_certs(untrusted_paths[i], &untrusted, NULL);
652 		if (ret > 0)
653 			n += ret;
654 	}
655 
656 	/* load installed untrusted certs */
657 	ret = read_certs(untrusted_dest, &untrusted, NULL);
658 	if (ret > 0)
659 		n += ret;
660 
661 	/* load legacy untrusted certs */
662 	path = expand_path(LEGACY_PATH);
663 	ret = read_certs(path, &untrusted, NULL);
664 	if (ret > 0) {
665 		warnx("certificates found in legacy directory %s",
666 		    path);
667 		n += ret;
668 	} else if (ret == 0) {
669 		warnx("legacy directory %s can safely be deleted",
670 		    path);
671 	}
672 	free(path);
673 
674 	info("%d untrusted certificates found", n);
675 	return (n);
676 }
677 
678 /*
679  * Save trusted certificates.
680  *
681  * Returns 0 on success and -1 on failure.
682  */
683 static int
684 save_trusted(void)
685 {
686 	int ret;
687 
688 	/* save untrusted certs */
689 	ret = write_certs(trusted_dest, &trusted);
690 	return (ret);
691 }
692 
693 /*
694  * Save untrusted certificates.
695  *
696  * Returns 0 on success and -1 on failure.
697  */
698 static int
699 save_untrusted(void)
700 {
701 	int ret;
702 
703 	ret = write_certs(untrusted_dest, &untrusted);
704 	return (ret);
705 }
706 
707 /*
708  * Save certificate bundle.
709  *
710  * Returns 0 on success and -1 on failure.
711  */
712 static int
713 save_bundle(void)
714 {
715 	char *dir, *file, *sep;
716 	int ret;
717 
718 	if ((sep = strrchr(bundle_dest, '/')) == NULL) {
719 		dir = NULL;
720 		file = bundle_dest;
721 	} else {
722 		dir = xasprintf("%.*s", (int)(sep - bundle_dest), bundle_dest);
723 		file = sep + 1;
724 	}
725 	ret = write_bundle(dir, file, &trusted);
726 	free(dir);
727 	return (ret);
728 }
729 
730 /*
731  * Save everything.
732  *
733  * Returns 0 on success and -1 on failure.
734  */
735 static int
736 save_all(void)
737 {
738 	int ret = 0;
739 
740 	ret |= save_untrusted();
741 	ret |= save_trusted();
742 	if (!nobundle)
743 		ret |= save_bundle();
744 	return (ret);
745 }
746 
747 /*
748  * List the contents of a certificate tree.
749  */
750 static void
751 list_certs(struct cert_tree *tree)
752 {
753 	struct cert *cert;
754 	char *path, *name;
755 
756 	RB_FOREACH(cert, cert_tree, tree) {
757 		path = longnames ? NULL : strrchr(cert->path, '/');
758 		name = longnames ? NULL : strrchr(cert->name, '=');
759 		printf("%s\t%s\n", path ? path + 1 : cert->path,
760 		    name ? name + 1 : cert->name);
761 	}
762 }
763 
764 /*
765  * Load installed trusted certificates, then list them.
766  *
767  * Returns 0 on success and -1 on failure.
768  */
769 static int
770 certctl_list(int argc, char **argv __unused)
771 {
772 	if (argc > 1)
773 		usage();
774 	/* load trusted certificates */
775 	load_trusted(false, NULL);
776 	/* list them */
777 	list_certs(&trusted);
778 	free_certs(&trusted);
779 	return (0);
780 }
781 
782 /*
783  * Load installed untrusted certificates, then list them.
784  *
785  * Returns 0 on success and -1 on failure.
786  */
787 static int
788 certctl_untrusted(int argc, char **argv __unused)
789 {
790 	if (argc > 1)
791 		usage();
792 	/* load untrusted certificates */
793 	load_untrusted(false);
794 	/* list them */
795 	list_certs(&untrusted);
796 	free_certs(&untrusted);
797 	return (0);
798 }
799 
800 /*
801  * Load trusted and untrusted certificates from all sources, then
802  * regenerate both the hashed directories and the bundle.
803  *
804  * Returns 0 on success and -1 on failure.
805  */
806 static int
807 certctl_rehash(int argc, char **argv __unused)
808 {
809 	int ret;
810 
811 	if (argc > 1)
812 		usage();
813 
814 	if (unprivileged && (mlf = fopen(metalog, "a")) == NULL) {
815 		warn("%s", metalog);
816 		return (-1);
817 	}
818 
819 	/* load untrusted certs first */
820 	load_untrusted(true);
821 
822 	/* load trusted certs, excluding any that are already untrusted */
823 	load_trusted(true, &untrusted);
824 
825 	/* save everything */
826 	ret = save_all();
827 
828 	/* clean up */
829 	free_certs(&untrusted);
830 	free_certs(&trusted);
831 	if (mlf != NULL)
832 		fclose(mlf);
833 	return (ret);
834 }
835 
836 /*
837  * Manually add one or more certificates to the list of trusted certificates.
838  *
839  * Returns 0 on success and -1 on failure.
840  */
841 static int
842 certctl_trust(int argc, char **argv)
843 {
844 	struct cert_tree extra = RB_INITIALIZER(&extra);
845 	struct cert *cert, *other, *tmp;
846 	unsigned int n;
847 	int i, ret;
848 
849 	if (argc < 2)
850 		usage();
851 
852 	/* load untrusted certs first */
853 	load_untrusted(true);
854 
855 	/* load trusted certs, excluding any that are already untrusted */
856 	load_trusted(true, &untrusted);
857 
858 	/* now load the additional trusted certificates */
859 	n = 0;
860 	for (i = 1; i < argc; i++) {
861 		ret = read_cert(argv[i], &extra, &trusted);
862 		if (ret > 0)
863 			n += ret;
864 	}
865 	if (n == 0) {
866 		warnx("no new trusted certificates found");
867 		free_certs(&untrusted);
868 		free_certs(&trusted);
869 		free_certs(&extra);
870 		return (0);
871 	}
872 
873 	/*
874 	 * For each new trusted cert, move it from the extra list to the
875 	 * trusted list, then check if a matching certificate exists on
876 	 * the untrusted list.  If that is the case, warn the user, then
877 	 * remove the matching certificate from the untrusted list.
878 	 */
879 	RB_FOREACH_SAFE(cert, cert_tree, &extra, tmp) {
880 		RB_REMOVE(cert_tree, &extra, cert);
881 		RB_INSERT(cert_tree, &trusted, cert);
882 		if ((other = RB_FIND(cert_tree, &untrusted, cert)) != NULL) {
883 			warnx("%s was previously untrusted", cert->name);
884 			RB_REMOVE(cert_tree, &untrusted, other);
885 			free_cert(other);
886 		}
887 	}
888 
889 	/* save everything */
890 	ret = save_all();
891 
892 	/* clean up */
893 	free_certs(&untrusted);
894 	free_certs(&trusted);
895 	return (ret);
896 }
897 
898 /*
899  * Manually add one or more certificates to the list of untrusted
900  * certificates.
901  *
902  * Returns 0 on success and -1 on failure.
903  */
904 static int
905 certctl_untrust(int argc, char **argv)
906 {
907 	unsigned int n;
908 	int i, ret;
909 
910 	if (argc < 2)
911 		usage();
912 
913 	/* load untrusted certs first */
914 	load_untrusted(true);
915 
916 	/* now load the additional untrusted certificates */
917 	n = 0;
918 	for (i = 1; i < argc; i++) {
919 		ret = read_cert(argv[i], &untrusted, NULL);
920 		if (ret > 0)
921 			n += ret;
922 	}
923 	if (n == 0) {
924 		warnx("no new untrusted certificates found");
925 		free_certs(&untrusted);
926 		return (0);
927 	}
928 
929 	/* load trusted certs, excluding any that are already untrusted */
930 	load_trusted(true, &untrusted);
931 
932 	/* save everything */
933 	ret = save_all();
934 
935 	/* clean up */
936 	free_certs(&untrusted);
937 	free_certs(&trusted);
938 	return (ret);
939 }
940 
941 static void
942 set_defaults(void)
943 {
944 	const char *value;
945 	char *str;
946 	size_t len;
947 
948 	if (localbase == NULL &&
949 	    (localbase = getenv("LOCALBASE")) == NULL) {
950 		if ((str = malloc((len = PATH_MAX) + 1)) == NULL)
951 			err(1, NULL);
952 		while (sysctlbyname("user.localbase", str, &len, NULL, 0) < 0) {
953 			if (errno != ENOMEM)
954 				err(1, "sysctl(user.localbase)");
955 			if ((str = realloc(str, len + 1)) == NULL)
956 				err(1, NULL);
957 		}
958 		str[len] = '\0';
959 		localbase = str;
960 	}
961 
962 	if (destdir == NULL &&
963 	    (destdir = getenv("DESTDIR")) == NULL)
964 		destdir = "";
965 	destdir = normalize_path(destdir);
966 
967 	if (distbase == NULL &&
968 	    (distbase = getenv("DISTBASE")) == NULL)
969 		distbase = "";
970 	if (*distbase != '\0' && *distbase != '/')
971 		errx(1, "DISTBASE=%s does not begin with a slash", distbase);
972 	distbase = normalize_path(distbase);
973 
974 	if (unprivileged && metalog == NULL &&
975 	    (metalog = getenv("METALOG")) == NULL)
976 		metalog = xasprintf("%s/METALOG", destdir);
977 
978 	if (!verbose) {
979 		if ((value = getenv("CERTCTL_VERBOSE")) != NULL) {
980 			if (value[0] != '\0') {
981 				verbose = true;
982 			}
983 		}
984 	}
985 
986 	if ((value = getenv("TRUSTPATH")) != NULL)
987 		trusted_paths = split_paths(value);
988 	else
989 		trusted_paths = expand_paths(default_trusted_paths);
990 
991 	if ((value = getenv("UNTRUSTPATH")) != NULL)
992 		untrusted_paths = split_paths(value);
993 	else
994 		untrusted_paths = expand_paths(default_untrusted_paths);
995 
996 	if ((value = getenv("TRUSTDESTDIR")) != NULL ||
997 	    (value = getenv("CERTDESTDIR")) != NULL)
998 		trusted_dest = xstrdup(value);
999 	else
1000 		trusted_dest = expand_path(TRUSTED_PATH);
1001 
1002 	if ((value = getenv("UNTRUSTDESTDIR")) != NULL)
1003 		untrusted_dest = xstrdup(value);
1004 	else
1005 		untrusted_dest = expand_path(UNTRUSTED_PATH);
1006 
1007 	if ((value = getenv("BUNDLE")) != NULL)
1008 		bundle_dest = xstrdup(value);
1009 	else
1010 		bundle_dest = expand_path(BUNDLE_PATH);
1011 
1012 	info("localbase:\t%s", localbase);
1013 	info("destdir:\t%s", destdir);
1014 	info("distbase:\t%s", distbase);
1015 	info("unprivileged:\t%s", unprivileged ? "true" : "false");
1016 	info("verbose:\t%s", verbose ? "true" : "false");
1017 }
1018 
1019 typedef int (*main_t)(int, char **);
1020 
1021 static struct {
1022 	const char	*name;
1023 	main_t		 func;
1024 } commands[] = {
1025 	{ "list",	certctl_list },
1026 	{ "untrusted",	certctl_untrusted },
1027 	{ "rehash",	certctl_rehash },
1028 	{ "untrust",	certctl_untrust },
1029 	{ "trust",	certctl_trust },
1030 	{ 0 },
1031 };
1032 
1033 static void
1034 usage(void)
1035 {
1036 	fprintf(stderr, "usage: certctl [-lv] [-D destdir] [-d distbase] list\n"
1037 	    "       certctl [-lv] [-D destdir] [-d distbase] untrusted\n"
1038 	    "       certctl [-BnUv] [-D destdir] [-d distbase] [-M metalog] rehash\n"
1039 	    "       certctl [-nv] [-D destdir] [-d distbase] untrust <file>\n"
1040 	    "       certctl [-nv] [-D destdir] [-d distbase] trust <file>\n");
1041 	exit(1);
1042 }
1043 
1044 int
1045 main(int argc, char *argv[])
1046 {
1047 	const char *command;
1048 	int opt;
1049 
1050 	while ((opt = getopt(argc, argv, "BcD:d:g:lL:M:no:Uv")) != -1)
1051 		switch (opt) {
1052 		case 'B':
1053 			nobundle = true;
1054 			break;
1055 		case 'c':
1056 			/* ignored for compatibility */
1057 			break;
1058 		case 'D':
1059 			destdir = optarg;
1060 			break;
1061 		case 'd':
1062 			distbase = optarg;
1063 			break;
1064 		case 'g':
1065 			gname = optarg;
1066 			break;
1067 		case 'l':
1068 			longnames = true;
1069 			break;
1070 		case 'L':
1071 			localbase = optarg;
1072 			break;
1073 		case 'M':
1074 			metalog = optarg;
1075 			break;
1076 		case 'n':
1077 			dryrun = true;
1078 			break;
1079 		case 'o':
1080 			uname = optarg;
1081 			break;
1082 		case 'U':
1083 			unprivileged = true;
1084 			break;
1085 		case 'v':
1086 			verbose = true;
1087 			break;
1088 		default:
1089 			usage();
1090 		}
1091 
1092 	argc -= optind;
1093 	argv += optind;
1094 
1095 	if (argc < 1)
1096 		usage();
1097 
1098 	command = *argv;
1099 
1100 	if ((nobundle || unprivileged || metalog != NULL) &&
1101 	    strcmp(command, "rehash") != 0)
1102 		usage();
1103 	if (!unprivileged && metalog != NULL) {
1104 		warnx("-M may only be used in conjunction with -U");
1105 		usage();
1106 	}
1107 
1108 	set_defaults();
1109 
1110 	for (unsigned i = 0; commands[i].name != NULL; i++)
1111 		if (strcmp(command, commands[i].name) == 0)
1112 			exit(!!commands[i].func(argc, argv));
1113 	usage();
1114 }
1115