diff --git a/usr.sbin/certctl/Makefile b/usr.sbin/certctl/Makefile index 8f19bde8aaf6..5430dbf24853 100644 --- a/usr.sbin/certctl/Makefile +++ b/usr.sbin/certctl/Makefile @@ -1,14 +1,10 @@ .include PACKAGE= certctl PROG= certctl MAN= certctl.8 -LIBADD= crypto util +LIBADD= crypto HAS_TESTS= SUBDIR.${MK_TESTS}= tests -.ifdef BOOTSTRAPPING -CFLAGS+=-DBOOTSTRAPPING -.endif - .include diff --git a/usr.sbin/certctl/certctl.8 b/usr.sbin/certctl/certctl.8 index c53ad9765544..97bdc840c359 100644 --- a/usr.sbin/certctl/certctl.8 +++ b/usr.sbin/certctl/certctl.8 @@ -1,163 +1,158 @@ .\" .\" SPDX-License-Identifier: BSD-2-Clause .\" .\" Copyright 2018 Allan Jude .\" .\" Redistribution and use in source and binary forms, with or without .\" modification, are permitted providing that the following conditions .\" are met: .\" 1. Redistributions of source code must retain the above copyright .\" notice, this list of conditions and the following disclaimer. .\" 2. Redistributions in binary form must reproduce the above copyright .\" notice, this list of conditions and the following disclaimer in the .\" documentation and/or other materials provided with the distribution. .\" .\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR .\" IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED .\" WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE .\" ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY .\" DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL .\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS .\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) .\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, .\" STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING .\" IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE .\" POSSIBILITY OF SUCH DAMAGE. .\" .Dd August 11, 2025 .Dt CERTCTL 8 .Os .Sh NAME .Nm certctl .Nd "tool for managing trusted and untrusted TLS certificates" .Sh SYNOPSIS .Nm .Op Fl lv .Ic list .Nm .Op Fl lv .Ic untrusted .Nm -.Op Fl BNnUv +.Op Fl BnUv .Op Fl D Ar destdir .Op Fl M Ar metalog .Ic rehash .Nm .Op Fl nv .Ic untrust Ar .Nm .Op Fl nv .Ic trust Ar .Sh DESCRIPTION The .Nm utility manages the list of TLS Certificate Authorities that are trusted by applications that use OpenSSL. .Pp The following options are available: .Bl -tag -width 4n .It Fl B Do not generate a bundle. This option is only valid in conjunction with the .Ic rehash command. .It Fl D Ar destdir Specify the DESTDIR (overriding values from the environment). .It Fl l When listing installed (trusted or untrusted) certificates, show the full path and distinguished name for each certificate. .It Fl M Ar metalog Specify the path of the METALOG file .Po default: .Pa ${DESTDIR}/METALOG .Pc . This option is only valid in conjunction with the .Ic rehash command. -.It Fl N -Base the file name on the certificate's name instead of its hash. -This option is only valid in conjunction with the -.Ic rehash -command. .It Fl n Dry-run mode. Do not actually perform any actions except write the metalog. .It Fl v Verbose mode. Print detailed information about each action taken. .It Fl U Unprivileged mode. Do not attempt to set the ownership of created files. This option is only valid in conjunction with the .Fl M option and the .Ic rehash command. .El .Pp Primary command functions: .Bl -tag -width untrusted .It Ic list List all currently trusted certificates. .It Ic untrusted List all currently untrusted certificates. .It Ic rehash Rebuild the list of trusted certificates by scanning all directories in .Ev TRUSTPATH and all untrusted certificates in .Ev UNTRUSTPATH . A copy of each trusted certificate is placed in .Ev CERTDESTDIR and each untrusted certificate in .Ev UNTRUSTDESTDIR . In addition, a bundle containing the trusted certificates is placed in .Ev BUNDLEFILE . .It Ic untrust Add the specified file to the untrusted list. .It Ic trust Remove the specified file from the untrusted list. .El .Sh ENVIRONMENT .Bl -tag -width UNTRUSTDESTDIR .It Ev DESTDIR Alternate destination directory to operate on. .It Ev LOCALBASE Location for local programs. Defaults to the value of the user.localbase sysctl which is usually .Pa /usr/local . .It Ev TRUSTPATH List of paths to search for trusted certificates. Default: .Pa ${DESTDIR}/usr/share/certs/trusted .Pa ${DESTDIR}${LOCALBASE}/share/certs/trusted .Pa ${DESTDIR}${LOCALBASE}/share/certs .It Ev UNTRUSTPATH List of paths to search for untrusted certificates. Default: .Pa ${DESTDIR}/usr/share/certs/untrusted .Pa ${DESTDIR}${LOCALBASE}/share/certs/untrusted .It Ev TRUSTDESTDIR Destination directory for symbolic links to trusted certificates. Default: .Pa ${DESTDIR}/etc/ssl/certs .It Ev UNTRUSTDESTDIR Destination directory for symbolic links to untrusted certificates. Default: .Pa ${DESTDIR}/etc/ssl/untrusted .It Ev BUNDLE File name of bundle to produce. .El .Sh SEE ALSO .Xr openssl 1 .Sh HISTORY .Nm first appeared in .Fx 12.2 . .Sh AUTHORS .An -nosplit The original shell implementation was written by .An Allan Jude Aq Mt allanjude@FreeBSD.org . The current C implementation was written by .An Dag-Erling Sm\(/orgrav Aq Mt des@FreeBSD.org . diff --git a/usr.sbin/certctl/certctl.c b/usr.sbin/certctl/certctl.c index f5876736d604..6687e56f23b4 100644 --- a/usr.sbin/certctl/certctl.c +++ b/usr.sbin/certctl/certctl.c @@ -1,1098 +1,1060 @@ /*- * Copyright (c) 2023-2025 Dag-Erling Smørgrav * * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include #include #include #include #include #include -#include -#include #include #include #include #include #include #include -#include #include #define info(fmt, ...) \ do { \ if (verbose) \ fprintf(stderr, fmt "\n", ##__VA_ARGS__); \ } while (0) static char * xasprintf(const char *fmt, ...) { va_list ap; char *str; int ret; va_start(ap, fmt); ret = vasprintf(&str, fmt, ap); va_end(ap); if (ret < 0 || str == NULL) err(1, NULL); return (str); } static char * xstrdup(const char *str) { char *dup; if ((dup = strdup(str)) == NULL) err(1, NULL); return (dup); } static void usage(void); static bool dryrun; static bool longnames; static bool nobundle; -static bool nohash; static bool unprivileged; static bool verbose; static const char *localbase; static const char *destdir; static const char *metalog; static const char *uname = "root"; static const char *gname = "wheel"; static const char *const default_trusted_paths[] = { "/usr/share/certs/trusted", "%L/share/certs/trusted", "%L/share/certs", NULL }; static char **trusted_paths; static const char *const default_untrusted_paths[] = { "/usr/share/certs/untrusted", "%L/share/certs/untrusted", NULL }; static char **untrusted_paths; static char *trusted_dest; static char *untrusted_dest; static char *bundle_dest; #define SSL_PATH "/etc/ssl" #define TRUSTED_DIR "certs" #define TRUSTED_PATH SSL_PATH "/" TRUSTED_DIR #define UNTRUSTED_DIR "untrusted" #define UNTRUSTED_PATH SSL_PATH "/" UNTRUSTED_DIR #define LEGACY_DIR "blacklisted" #define LEGACY_PATH SSL_PATH "/" LEGACY_DIR #define BUNDLE_FILE "cert.pem" #define BUNDLE_PATH SSL_PATH "/" BUNDLE_FILE static FILE *mlf; /* * Split a colon-separated list into a NULL-terminated array. */ static char ** split_paths(const char *str) { char **paths; const char *p, *q; unsigned int i, n; for (p = str, n = 1; *p; p++) { if (*p == ':') n++; } if ((paths = calloc(n + 1, sizeof(*paths))) == NULL) err(1, NULL); for (p = q = str, i = 0; i < n; i++, p = q + 1) { q = strchrnul(p, ':'); if ((paths[i] = strndup(p, q - p)) == NULL) err(1, NULL); } return (paths); } /* * Expand %L into LOCALBASE and prefix DESTDIR. */ static char * expand_path(const char *template) { if (template[0] == '%' && template[1] == 'L') return (xasprintf("%s%s%s", destdir, localbase, template + 2)); return (xasprintf("%s%s", destdir, template)); } /* * Expand an array of paths. */ static char ** expand_paths(const char *const *templates) { char **paths; unsigned int i, n; for (n = 0; templates[n] != NULL; n++) continue; if ((paths = calloc(n + 1, sizeof(*paths))) == NULL) err(1, NULL); for (i = 0; i < n; i++) paths[i] = expand_path(templates[i]); return (paths); } /* * If destdir is a prefix of path, returns a pointer to the rest of path, * otherwise returns path. */ static const char * unexpand_path(const char *path) { const char *p = path; const char *q = destdir; while (*p && *p == *q) { p++; q++; } return (*q == '\0' && *p == '/' ? p : path); } /* * X509 certificate in a rank-balanced tree. */ struct cert { RB_ENTRY(cert) entry; unsigned long hash; char *name; X509 *x509; char *path; }; static void free_cert(struct cert *cert) { free(cert->name); X509_free(cert->x509); free(cert->path); free(cert); } static int certcmp(const struct cert *a, const struct cert *b) { return (X509_cmp(a->x509, b->x509)); } RB_HEAD(cert_tree, cert); static struct cert_tree trusted = RB_INITIALIZER(&trusted); static struct cert_tree untrusted = RB_INITIALIZER(&untrusted); RB_GENERATE_STATIC(cert_tree, cert, entry, certcmp); static void free_certs(struct cert_tree *tree) { struct cert *cert, *tmp; RB_FOREACH_SAFE(cert, cert_tree, tree, tmp) { RB_REMOVE(cert_tree, tree, cert); free_cert(cert); } } static struct cert * find_cert(struct cert_tree *haystack, X509 *x509) { struct cert needle = { .x509 = x509 }; return (RB_FIND(cert_tree, haystack, &needle)); } /* * File containing a certificate in a rank-balanced tree sorted by * certificate hash and disambiguating counter. This is needed because * the certificate hash function is prone to collisions, necessitating a * counter to distinguish certificates that hash to the same value. */ struct file { RB_ENTRY(file) entry; const struct cert *cert; unsigned int c; }; static int filecmp(const struct file *a, const struct file *b) { if (a->cert->hash > b->cert->hash) return (1); if (a->cert->hash < b->cert->hash) return (-1); return (a->c - b->c); } RB_HEAD(file_tree, file); RB_GENERATE_STATIC(file_tree, file, entry, filecmp); /* * Lexicographical sort for scandir(). */ static int lexisort(const struct dirent **d1, const struct dirent **d2) { return (strcmp((*d1)->d_name, (*d2)->d_name)); } /* * Read certificate(s) from a single file and insert them into a tree. * Ignore certificates that already exist in the tree. If exclude is not * null, also ignore certificates that exist in exclude. * * Returns the number certificates added to the tree, or -1 on failure. */ static int read_cert(const char *path, struct cert_tree *tree, struct cert_tree *exclude) { FILE *f; X509 *x509; X509_NAME *name; struct cert *cert; unsigned long hash; int ni, no; if ((f = fopen(path, "r")) == NULL) { warn("%s", path); return (-1); } for (ni = no = 0; (x509 = PEM_read_X509(f, NULL, NULL, NULL)) != NULL; ni++) { hash = X509_subject_name_hash(x509); if (exclude && find_cert(exclude, x509)) { info("%08lx: excluded", hash); X509_free(x509); continue; } if (find_cert(tree, x509)) { info("%08lx: duplicate", hash); X509_free(x509); continue; } if ((cert = calloc(1, sizeof(*cert))) == NULL) err(1, NULL); cert->x509 = x509; name = X509_get_subject_name(x509); cert->hash = X509_NAME_hash_ex(name, NULL, NULL, NULL); cert->name = X509_NAME_oneline(name, NULL, 0); cert->path = xstrdup(unexpand_path(path)); if (RB_INSERT(cert_tree, tree, cert) != NULL) errx(1, "unexpected duplicate"); info("%08lx: %s", cert->hash, strrchr(cert->name, '=') + 1); no++; } /* * ni is the number of certificates we found in the file. * no is the number of certificates that weren't already in our * tree or on the exclusion list. */ if (ni == 0) warnx("%s: no valid certificates found", path); fclose(f); return (no); } /* * Load all certificates found in the specified path into a tree, * optionally excluding those that already exist in a different tree. * * Returns the number of certificates added to the tree, or -1 on failure. */ static int read_certs(const char *path, struct cert_tree *tree, struct cert_tree *exclude) { struct stat sb; char *paths[] = { (char *)(uintptr_t)path, NULL }; FTS *fts; FTSENT *ent; int fts_options = FTS_LOGICAL | FTS_NOCHDIR; int ret, total = 0; if (stat(path, &sb) != 0) { return (-1); } else if (!S_ISDIR(sb.st_mode)) { errno = ENOTDIR; return (-1); } if ((fts = fts_open(paths, fts_options, NULL)) == NULL) err(1, "fts_open()"); while ((ent = fts_read(fts)) != NULL) { if (ent->fts_info != FTS_F) { if (ent->fts_info == FTS_ERR) warnc(ent->fts_errno, "fts_read()"); continue; } info("found %s", ent->fts_path); ret = read_cert(ent->fts_path, tree, exclude); if (ret > 0) total += ret; } fts_close(fts); return (total); } /* * Save the contents of a cert tree to disk. * * Returns 0 on success and -1 on failure. */ static int write_certs(const char *dir, struct cert_tree *tree) { struct file_tree files = RB_INITIALIZER(&files); struct cert *cert; struct file *file, *tmp; struct dirent **dents, **ent; char *path, *tmppath = NULL; FILE *f; mode_t mode = 0444; int cmp, d, fd, ndents, ret = 0; /* * Start by generating unambiguous file names for each certificate * and storing them in lexicographical order */ RB_FOREACH(cert, cert_tree, tree) { if ((file = calloc(1, sizeof(*file))) == NULL) err(1, NULL); file->cert = cert; for (file->c = 0; file->c < INT_MAX; file->c++) if (RB_INSERT(file_tree, &files, file) == NULL) break; if (file->c == INT_MAX) errx(1, "unable to disambiguate %08lx", cert->hash); free(cert->path); - if (nohash) { - X509_NAME *xn; - X509_NAME_ENTRY *xe; - ASN1_STRING *as; - unsigned char *us = NULL; - int xi, usl; - - xn = X509_get_subject_name(cert->x509); - xi = X509_NAME_get_index_by_NID(xn, NID_commonName, -1); - if (xi < 0) { - warnx("%08lx.%d: certificate has no CN", - cert->hash, file->c); - xi = X509_NAME_get_index_by_NID(xn, - NID_organizationalUnitName, -1); - } - if (xi < 0) { - warnx("%08lx.%d: certificate has no OU", - cert->hash, file->c); - xi = X509_NAME_get_index_by_NID(xn, - NID_organizationName, -1); - } - if (xi < 0) { - warnx("%08lx.%d: certificate has no O", - cert->hash, file->c); - cert->path = xasprintf("%08lx.%d", cert->hash, - file->c); - } - xe = X509_NAME_get_entry(xn, xi); - as = X509_NAME_ENTRY_get_data(xe); - usl = ASN1_STRING_to_UTF8(&us, as); - if (usl < 0) { - errx(1, "%08lx.%d: %s", cert->hash, file->c, - ERR_error_string(ERR_get_error(), NULL)); - } - cert->path = xasprintf("%s.pem", (char *)us); - OPENSSL_free(us); - } else { - cert->path = xasprintf("%08lx.%d", cert->hash, file->c); - } + cert->path = xasprintf("%08lx.%d", cert->hash, file->c); } /* * Open and scan the directory. */ if ((d = open(dir, O_DIRECTORY | O_RDONLY)) < 0 || -#ifdef BOOTSTRAPPING - (ndents = scandir(dir, &dents, NULL, lexisort)) -#else - (ndents = fdscandir(d, &dents, NULL, lexisort)) -#endif - < 0) + (ndents = fdscandir(d, &dents, NULL, lexisort)) < 0) err(1, "%s", dir); - /* * Iterate over the directory listing and the certificate listing * in parallel. If the directory listing gets ahead of the * certificate listing, we need to write the current certificate * and advance the certificate listing. If the certificate * listing is ahead of the directory listing, we need to delete * the current file and advance the directory listing. If they * are neck and neck, we have a match and could in theory compare * the two, but in practice it's faster to just replace the * current file with the current certificate (and advance both). */ ent = dents; file = RB_MIN(file_tree, &files); for (;;) { if (ent < dents + ndents) { /* skip directories */ if ((*ent)->d_type == DT_DIR) { free(*ent++); continue; } if (file != NULL) { /* compare current dirent to current cert */ path = file->cert->path; cmp = strcmp((*ent)->d_name, path); } else { /* trailing files in directory */ path = NULL; cmp = -1; } } else { if (file != NULL) { /* trailing certificates */ path = file->cert->path; cmp = 1; } else { /* end of both lists */ path = NULL; break; } } if (cmp < 0) { /* a file on disk with no matching certificate */ info("removing %s/%s", dir, (*ent)->d_name); if (!dryrun) (void)unlinkat(d, (*ent)->d_name, 0); free(*ent++); continue; } if (cmp == 0) { /* a file on disk with a matching certificate */ info("replacing %s/%s", dir, (*ent)->d_name); if (dryrun) { fd = open(_PATH_DEVNULL, O_WRONLY); } else { tmppath = xasprintf(".%s", path); fd = openat(d, tmppath, O_CREAT | O_WRONLY | O_TRUNC, mode); if (!unprivileged && fd >= 0) (void)fchmod(fd, mode); } free(*ent++); } else { /* a certificate with no matching file */ info("writing %s/%s", dir, path); if (dryrun) { fd = open(_PATH_DEVNULL, O_WRONLY); } else { tmppath = xasprintf(".%s", path); fd = openat(d, tmppath, O_CREAT | O_WRONLY | O_EXCL, mode); } } /* write the certificate */ if (fd < 0 || (f = fdopen(fd, "w")) == NULL || !PEM_write_X509(f, file->cert->x509)) { if (tmppath != NULL && fd >= 0) { int serrno = errno; (void)unlinkat(d, tmppath, 0); errno = serrno; } err(1, "%s/%s", dir, tmppath ? tmppath : path); } /* rename temp file if applicable */ if (tmppath != NULL) { if (ret == 0 && renameat(d, tmppath, d, path) != 0) { warn("%s/%s", dir, path); ret = -1; } if (ret != 0) (void)unlinkat(d, tmppath, 0); free(tmppath); tmppath = NULL; } /* emit metalog */ if (mlf != NULL) { fprintf(mlf, "%s/%s type=file " "uname=%s gname=%s mode=%#o size=%ld\n", unexpand_path(dir), path, uname, gname, mode, ftell(f)); } fclose(f); /* advance certificate listing */ tmp = RB_NEXT(file_tree, &files, file); RB_REMOVE(file_tree, &files, file); free(file); file = tmp; } free(dents); close(d); return (ret); } /* * Save all certs in a tree to a single file (bundle). * * Returns 0 on success and -1 on failure. */ static int write_bundle(const char *dir, const char *file, struct cert_tree *tree) { struct cert *cert; char *tmpfile = NULL; FILE *f; int d, fd, ret = 0; mode_t mode = 0444; if (dir != NULL) { if ((d = open(dir, O_DIRECTORY | O_RDONLY)) < 0) err(1, "%s", dir); } else { dir = "."; d = AT_FDCWD; } info("writing %s/%s", dir, file); if (dryrun) { fd = open(_PATH_DEVNULL, O_WRONLY); } else { tmpfile = xasprintf(".%s", file); fd = openat(d, tmpfile, O_WRONLY | O_CREAT | O_EXCL, mode); } if (fd < 0 || (f = fdopen(fd, "w")) == NULL) { if (tmpfile != NULL && fd >= 0) { int serrno = errno; (void)unlinkat(d, tmpfile, 0); errno = serrno; } err(1, "%s/%s", dir, tmpfile ? tmpfile : file); } RB_FOREACH(cert, cert_tree, tree) { if (!PEM_write_X509(f, cert->x509)) { warn("%s/%s", dir, tmpfile ? tmpfile : file); ret = -1; break; } } if (tmpfile != NULL) { if (ret == 0 && renameat(d, tmpfile, d, file) != 0) { warn("%s/%s", dir, file); ret = -1; } if (ret != 0) (void)unlinkat(d, tmpfile, 0); free(tmpfile); } if (ret == 0 && mlf != NULL) { fprintf(mlf, "%s/%s type=file uname=%s gname=%s mode=%#o size=%ld\n", unexpand_path(dir), file, uname, gname, mode, ftell(f)); } fclose(f); if (d != AT_FDCWD) close(d); return (ret); } /* * Load trusted certificates. * * Returns the number of certificates loaded. */ static unsigned int load_trusted(bool all, struct cert_tree *exclude) { unsigned int i, n; int ret; /* load external trusted certs */ for (i = n = 0; all && trusted_paths[i] != NULL; i++) { ret = read_certs(trusted_paths[i], &trusted, exclude); if (ret > 0) n += ret; } /* load installed trusted certs */ ret = read_certs(trusted_dest, &trusted, exclude); if (ret > 0) n += ret; info("%d trusted certificates found", n); return (n); } /* * Load untrusted certificates. * * Returns the number of certificates loaded. */ static unsigned int -load_untrusted(bool all, struct cert_tree *exclude) +load_untrusted(bool all) { char *path; unsigned int i, n; int ret; /* load external untrusted certs */ for (i = n = 0; all && untrusted_paths[i] != NULL; i++) { - ret = read_certs(untrusted_paths[i], &untrusted, exclude); + ret = read_certs(untrusted_paths[i], &untrusted, NULL); if (ret > 0) n += ret; } /* load installed untrusted certs */ - ret = read_certs(untrusted_dest, &untrusted, exclude); + ret = read_certs(untrusted_dest, &untrusted, NULL); if (ret > 0) n += ret; /* load legacy untrusted certs */ path = expand_path(LEGACY_PATH); - ret = read_certs(path, &untrusted, exclude); + ret = read_certs(path, &untrusted, NULL); if (ret > 0) { warnx("certificates found in legacy directory %s", path); n += ret; } else if (ret == 0) { warnx("legacy directory %s can safely be deleted", path); } free(path); info("%d untrusted certificates found", n); return (n); } /* * Save trusted certificates. * * Returns 0 on success and -1 on failure. */ static int save_trusted(void) { int ret; /* save untrusted certs */ ret = write_certs(trusted_dest, &trusted); return (ret); } /* * Save untrusted certificates. * * Returns 0 on success and -1 on failure. */ static int save_untrusted(void) { int ret; ret = write_certs(untrusted_dest, &untrusted); return (ret); } /* * Save certificate bundle. * * Returns 0 on success and -1 on failure. */ static int save_bundle(void) { char *dir, *file, *sep; int ret; if ((sep = strrchr(bundle_dest, '/')) == NULL) { dir = NULL; file = bundle_dest; } else { dir = xasprintf("%.*s", (int)(sep - bundle_dest), bundle_dest); file = sep + 1; } ret = write_bundle(dir, file, &trusted); free(dir); return (ret); } /* * Save everything. * * Returns 0 on success and -1 on failure. */ static int save_all(void) { int ret = 0; ret |= save_untrusted(); ret |= save_trusted(); if (!nobundle) ret |= save_bundle(); return (ret); } /* * List the contents of a certificate tree. */ static void list_certs(struct cert_tree *tree) { struct cert *cert; char *path, *name; RB_FOREACH(cert, cert_tree, tree) { path = longnames ? NULL : strrchr(cert->path, '/'); name = longnames ? NULL : strrchr(cert->name, '='); printf("%s\t%s\n", path ? path + 1 : cert->path, name ? name + 1 : cert->name); } } /* * Load installed trusted certificates, then list them. * * Returns 0 on success and -1 on failure. */ static int certctl_list(int argc, char **argv __unused) { if (argc > 1) usage(); /* load trusted certificates */ load_trusted(false, NULL); /* list them */ list_certs(&trusted); free_certs(&trusted); return (0); } /* * Load installed untrusted certificates, then list them. * * Returns 0 on success and -1 on failure. */ static int certctl_untrusted(int argc, char **argv __unused) { if (argc > 1) usage(); /* load untrusted certificates */ - load_untrusted(false, NULL); + load_untrusted(false); /* list them */ list_certs(&untrusted); free_certs(&untrusted); return (0); } /* * Load trusted and untrusted certificates from all sources, then * regenerate both the hashed directories and the bundle. * * Returns 0 on success and -1 on failure. */ static int certctl_rehash(int argc, char **argv __unused) { int ret; if (argc > 1) usage(); if (unprivileged && (mlf = fopen(metalog, "a")) == NULL) { warn("%s", metalog); return (-1); } /* load untrusted certs first */ - load_untrusted(true, NULL); + load_untrusted(true); /* load trusted certs, excluding any that are already untrusted */ load_trusted(true, &untrusted); /* save everything */ ret = save_all(); /* clean up */ free_certs(&untrusted); free_certs(&trusted); if (mlf != NULL) fclose(mlf); return (ret); } /* * Manually add one or more certificates to the list of trusted certificates. * * Returns 0 on success and -1 on failure. */ static int certctl_trust(int argc, char **argv) { struct cert_tree extra = RB_INITIALIZER(&extra); struct cert *cert, *other, *tmp; unsigned int n; int i, ret; if (argc < 2) usage(); /* load untrusted certs first */ - load_untrusted(true, NULL); + load_untrusted(true); /* load trusted certs, excluding any that are already untrusted */ load_trusted(true, &untrusted); /* now load the additional trusted certificates */ n = 0; for (i = 1; i < argc; i++) { ret = read_cert(argv[i], &extra, &trusted); if (ret > 0) n += ret; } if (n == 0) { warnx("no new trusted certificates found"); free_certs(&untrusted); free_certs(&trusted); free_certs(&extra); return (0); } /* * For each new trusted cert, move it from the extra list to the * trusted list, then check if a matching certificate exists on * the untrusted list. If that is the case, warn the user, then * remove the matching certificate from the untrusted list. */ RB_FOREACH_SAFE(cert, cert_tree, &extra, tmp) { RB_REMOVE(cert_tree, &extra, cert); RB_INSERT(cert_tree, &trusted, cert); if ((other = RB_FIND(cert_tree, &untrusted, cert)) != NULL) { warnx("%s was previously untrusted", cert->name); RB_REMOVE(cert_tree, &untrusted, other); free_cert(other); } } /* save everything */ ret = save_all(); /* clean up */ free_certs(&untrusted); free_certs(&trusted); return (ret); } /* * Manually add one or more certificates to the list of untrusted * certificates. * * Returns 0 on success and -1 on failure. */ static int certctl_untrust(int argc, char **argv) { unsigned int n; int i, ret; if (argc < 2) usage(); /* load untrusted certs first */ - load_untrusted(true, NULL); + load_untrusted(true); /* now load the additional untrusted certificates */ n = 0; for (i = 1; i < argc; i++) { ret = read_cert(argv[i], &untrusted, NULL); if (ret > 0) n += ret; } if (n == 0) { warnx("no new untrusted certificates found"); free_certs(&untrusted); return (0); } /* load trusted certs, excluding any that are already untrusted */ load_trusted(true, &untrusted); /* save everything */ ret = save_all(); /* clean up */ free_certs(&untrusted); free_certs(&trusted); return (ret); } static void set_defaults(void) { const char *value; + char *str; + size_t len; if (localbase == NULL && - (localbase = getenv("LOCALBASE")) == NULL) - localbase = getlocalbase(); + (localbase = getenv("LOCALBASE")) == NULL) { + if ((str = malloc((len = PATH_MAX) + 1)) == NULL) + err(1, NULL); + while (sysctlbyname("user.localbase", str, &len, NULL, 0) < 0) { + if (errno != ENOMEM) + err(1, "sysctl(user.localbase)"); + if ((str = realloc(str, len + 1)) == NULL) + err(1, NULL); + } + str[len] = '\0'; + localbase = str; + } if (destdir == NULL && (destdir = getenv("DESTDIR")) == NULL) destdir = ""; if (unprivileged && metalog == NULL && (metalog = getenv("METALOG")) == NULL) metalog = xasprintf("%s/METALOG", destdir); if (!verbose) { if ((value = getenv("CERTCTL_VERBOSE")) != NULL) { if (value[0] != '\0') { verbose = true; } } } if ((value = getenv("TRUSTPATH")) != NULL) trusted_paths = split_paths(value); else trusted_paths = expand_paths(default_trusted_paths); if ((value = getenv("UNTRUSTPATH")) != NULL) untrusted_paths = split_paths(value); else untrusted_paths = expand_paths(default_untrusted_paths); if ((value = getenv("TRUSTDESTDIR")) != NULL || (value = getenv("CERTDESTDIR")) != NULL) trusted_dest = xstrdup(value); else trusted_dest = expand_path(TRUSTED_PATH); if ((value = getenv("UNTRUSTDESTDIR")) != NULL) untrusted_dest = xstrdup(value); else untrusted_dest = expand_path(UNTRUSTED_PATH); if ((value = getenv("BUNDLE")) != NULL) bundle_dest = xstrdup(value); else bundle_dest = expand_path(BUNDLE_PATH); info("localbase:\t%s", localbase); info("destdir:\t%s", destdir); info("unprivileged:\t%s", unprivileged ? "true" : "false"); info("verbose:\t%s", verbose ? "true" : "false"); } typedef int (*main_t)(int, char **); static struct { const char *name; main_t func; } commands[] = { { "list", certctl_list }, { "untrusted", certctl_untrusted }, { "rehash", certctl_rehash }, { "untrust", certctl_untrust }, { "trust", certctl_trust }, { 0 }, }; static void usage(void) { fprintf(stderr, "usage: certctl [-lv] [-D destdir] list\n" " certctl [-lv] [-D destdir] untrusted\n" - " certctl [-BNnUv] [-D destdir] [-M metalog] rehash\n" + " certctl [-BnUv] [-D destdir] [-M metalog] rehash\n" " certctl [-nv] [-D destdir] untrust \n" " certctl [-nv] [-D destdir] trust \n"); exit(1); } int main(int argc, char *argv[]) { const char *command; int opt; - while ((opt = getopt(argc, argv, "BcD:g:lL:M:Nno:Uv")) != -1) + while ((opt = getopt(argc, argv, "BcD:g:lL:M:no:Uv")) != -1) switch (opt) { case 'B': nobundle = true; break; case 'c': /* ignored for compatibility */ break; case 'D': destdir = optarg; break; case 'g': gname = optarg; break; case 'l': longnames = true; break; case 'L': localbase = optarg; break; case 'M': metalog = optarg; break; - case 'N': - nohash = true; - break; case 'n': dryrun = true; break; case 'o': uname = optarg; break; case 'U': unprivileged = true; break; case 'v': verbose = true; break; default: usage(); } argc -= optind; argv += optind; if (argc < 1) usage(); command = *argv; - if ((nobundle || nohash || unprivileged || metalog != NULL) && + if ((nobundle || unprivileged || metalog != NULL) && strcmp(command, "rehash") != 0) usage(); if (!unprivileged && metalog != NULL) { warnx("-M may only be used in conjunction with -U"); usage(); } set_defaults(); for (unsigned i = 0; commands[i].name != NULL; i++) if (strcmp(command, commands[i].name) == 0) exit(!!commands[i].func(argc, argv)); usage(); }