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