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