Page MenuHomeFreeBSD

D42320.id129218.diff
No OneTemporary

D42320.id129218.diff

diff --git a/usr.sbin/certctl/Makefile b/usr.sbin/certctl/Makefile
--- a/usr.sbin/certctl/Makefile
+++ b/usr.sbin/certctl/Makefile
@@ -1,6 +1,6 @@
-
PACKAGE= certctl
-SCRIPTS=certctl.sh
+PROG= certctl
MAN= certctl.8
+LIBADD= crypto
.include <bsd.prog.mk>
diff --git a/usr.sbin/certctl/certctl.8 b/usr.sbin/certctl/certctl.8
--- a/usr.sbin/certctl/certctl.8
+++ b/usr.sbin/certctl/certctl.8
@@ -24,7 +24,7 @@
.\" IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
.\" POSSIBILITY OF SUCH DAMAGE.
.\"
-.Dd July 13, 2022
+.Dd October 22, 2023
.Dt CERTCTL 8
.Os
.Sh NAME
@@ -32,10 +32,10 @@
.Nd "tool for managing trusted and untrusted TLS certificates"
.Sh SYNOPSIS
.Nm
-.Op Fl v
+.Op Fl lv
.Ic list
.Nm
-.Op Fl v
+.Op Fl lv
.Ic untrusted
.Nm
.Op Fl nUv
@@ -60,33 +60,40 @@
Specify the DESTDIR (overriding values from the environment).
.It Fl d Ar distbase
Specify the DISTBASE (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 (default: $DESTDIR/METALOG).
.It Fl n
-No-Op mode, do not actually perform any actions.
+Dryrun mode.
+Do not actually perform any actions.
.It Fl v
-Be verbose, print details about actions before performing them.
+Verbose mode.
+Print detailed information about each action taken.
.It Fl U
-Unprivileged mode, do not change the ownership of created links.
-Do record the ownership in the METALOG file.
+Unprivileged mode.
+Record each file created in the METALOG.
.El
.Pp
Primary command functions:
.Bl -tag -width untrusted
.It Ic list
-List all currently trusted certificate authorities.
+List all currently trusted certificates.
.It Ic untrusted
List all currently untrusted certificates.
.It Ic rehash
-Rebuild the list of trusted certificate authorities by scanning all directories
+Rebuild the list of trusted certificates by scanning all directories
in
.Ev TRUSTPATH
and all untrusted certificates in
.Ev UNTRUSTPATH .
-A symbolic link to each trusted certificate is placed in
+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
diff --git a/usr.sbin/certctl/certctl.c b/usr.sbin/certctl/certctl.c
new file mode 100644
--- /dev/null
+++ b/usr.sbin/certctl/certctl.c
@@ -0,0 +1,690 @@
+/*-
+ * Copyright (c) 2023 Dag-Erling Smørgrav
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <sys/param.h>
+#include <sys/sysctl.h>
+#include <sys/stat.h>
+#include <sys/tree.h>
+
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <fts.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <openssl/ssl.h>
+
+#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 unprivileged;
+static bool verbose;
+
+static const char *localbase;
+static const char *destdir;
+static const char *distbase;
+static const char *metalog;
+
+static const char *const trusted_paths[] = {
+ "/usr/share/certs/trusted",
+ "%L/share/certs",
+ "%L/etc/ssl/certs",
+};
+
+static const char *const untrusted_paths[] = {
+ "/usr/share/certs/untrusted",
+ "%L/etc/ssl/untrusted",
+ "%L/etc/ssl/blacklisted",
+};
+
+#define SSL_PATH "/etc/ssl"
+#define TRUSTED_SUBDIR "certs"
+#define TRUSTED_PATH SSL_PATH "/" TRUSTED_SUBDIR
+#define UNTRUSTED_SUBDIR "untrusted"
+#define UNTRUSTED_PATH SSL_PATH "/" UNTRUSTED_SUBDIR
+#define LEGACY_SUBDIR "blacklisted"
+#define LEGACY_PATH SSL_PATH "/" LEGACY_SUBDIR
+#define BUNDLE_FILE "cert.pem"
+#define BUNDLE_PATH SSL_PATH "/" BUNDLE_FILE
+
+static FILE *mlf;
+
+static char *
+expand_path(const char *path)
+{
+ if (path[0] == '%' && path[1] == 'L')
+ return (xasprintf("%s%s%s", destdir, localbase, path + 2));
+ return (xasprintf("%s%s%s", destdir, distbase, path));
+}
+
+/*
+ * 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);
+}
+
+/*
+ * Create a directory and all its ancestors. The string must be writable.
+ */
+static int
+mkpath(char *path)
+{
+ struct stat st;
+ char *sep;
+ int ret;
+
+ if ((ret = mkdir(path, 0755)) == 0)
+ return (ret);
+ if (errno == EEXIST && stat(path, &st) == 0) {
+ if (S_ISDIR(st.st_mode))
+ return (0);
+ errno = EEXIST;
+ return (-1);
+ }
+ if ((sep = strrchr(path, '/')) == NULL) {
+ errno = EINVAL;
+ return (-1);
+ }
+ while (sep > path && *sep == '/')
+ sep--;
+ if (sep > path) {
+ sep[1] = '\0';
+ ret = mkpath(path);
+ sep[1] = '/';
+ if (ret != 0)
+ return (ret);
+ }
+ info("creating %s", path);
+ return (mkdir(path, 0755));
+}
+
+/*
+ * Delete a directory and its contents recursively.
+ */
+static int
+rmpath(const char *path)
+{
+ const char *paths[] = { path, NULL };
+ FTS *fts;
+ FTSENT *ent;
+ int fts_options = FTS_PHYSICAL | FTS_NOSTAT;
+ int ret = 0;
+
+ if ((fts = fts_open((char * const *)(uintptr_t)paths, fts_options, NULL)) == NULL)
+ err(1, "fts_open()");
+ while ((ent = fts_read(fts)) != NULL) {
+ switch (ent->fts_info) {
+ case FTS_ERR:
+ warnc(ent->fts_errno, "fts_read()");
+ break;
+ case FTS_DNR:
+ warnc(ent->fts_errno, "%s", ent->fts_accpath);
+ break;
+ case FTS_D:
+ /* ignore */
+ break;
+ case FTS_DP:
+ default:
+ info("removing %s", ent->fts_path);
+ if (remove(ent->fts_accpath) != 0 && errno != ENOENT) {
+ warn("%s", ent->fts_accpath);
+ ret = -1;
+ }
+ break;
+ }
+ }
+ fts_close(fts);
+ return (ret);
+}
+
+/*
+ * 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));
+}
+
+static RB_HEAD(cert_tree, cert) trusted, 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));
+}
+
+/*
+ * Load all certificates found in the specified path into a tree,
+ * optionally excluding those that already exist in a different tree.
+ */
+static unsigned int
+load_certs(const char *path, struct cert_tree *tree, struct cert_tree *exclude)
+{
+ char *paths[] = { (char *)(uintptr_t)path, NULL };
+ FTS *fts;
+ FTSENT *ent;
+ FILE *f;
+ X509 *x509;
+ struct cert *cert;
+ unsigned long hash;
+ int fts_options = FTS_LOGICAL | FTS_NOCHDIR;
+ int n, total = 0;
+
+ 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_accpath);
+ if ((f = fopen(ent->fts_accpath, "r")) == NULL) {
+ warn("%s", ent->fts_accpath);
+ continue;
+ }
+ for (n = 0; (x509 = PEM_read_X509(f, NULL, NULL, NULL)); n++) {
+ 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;
+ cert->hash = X509_subject_name_hash(x509);
+ cert->name = X509_NAME_oneline(X509_get_subject_name(x509), NULL, 0);
+ cert->path = xstrdup(unexpand_path(ent->fts_path));
+ if (RB_INSERT(cert_tree, tree, cert) != NULL)
+ errx(1, "unexpected duplicate");
+ info("%08lx: %s", cert->hash, strrchr(cert->name, '=') + 1);
+ total++;
+ }
+ if (n == 0)
+ warnx("%s: no valid certificates found", ent->fts_accpath);
+ fclose(f);
+ }
+ fts_close(fts);
+ return (total);
+}
+
+/*
+ * Save the contents of a cert tree to disk
+ */
+static int
+save_certs(const char *dir, const char *subdir, struct cert_tree *tree)
+{
+ char *path;
+ struct cert *cert;
+ FILE *f;
+ long len;
+ int c, fd, ret;
+ mode_t mode = 0444;
+
+ path = xasprintf("%s/%s", dir, subdir);
+ if (mkpath(path) != 0)
+ return (-1);
+ if ((len = pathconf(dir, _PC_PATH_MAX)) < 0)
+ len = PATH_MAX;
+ len++;
+ RB_FOREACH(cert, cert_tree, tree) {
+ free(cert->path);
+ if ((cert->path = malloc(len)) == NULL) {
+ warn(NULL);
+ ret = -1;
+ break;
+ }
+ c = 0;
+again:
+ if (snprintf(cert->path, len, "%s/%08lx.%d", path, cert->hash, c) >= len) {
+ warnc(ENAMETOOLONG, "%s/%08lx.%d", path, cert->hash, c);
+ free(cert->path);
+ cert->path = NULL;
+ ret = -1;
+ continue;
+ }
+ if ((fd = open(cert->path, O_WRONLY|O_CREAT|O_EXCL, mode)) < 0) {
+ if (errno == EEXIST) {
+ c++;
+ goto again;
+ }
+ warn("%s", cert->path);
+ ret = -1;
+ continue;
+ }
+ info("writing %s", cert->path);
+ if ((f = fdopen(fd, "w")) == NULL)
+ err(1, NULL);
+ if (!PEM_write_X509(f, cert->x509)) {
+ warn("%s", cert->path);
+ fclose(f);
+ ret = -1;
+ continue;
+ }
+ if (mlf != NULL) {
+ fprintf(mlf,
+ "%s type=file mode=%#o size=%ld\n",
+ unexpand_path(cert->path), mode, ftell(f));
+ }
+ fclose(f);
+ }
+ free(path);
+ return (ret);
+}
+
+/*
+ * Save all certs in a tree to a single file (bundle)
+ */
+static int
+save_bundle(const char *dir, const char *file, struct cert_tree *tree)
+{
+ struct cert *cert;
+ char *path;
+ FILE *f;
+ int fd, ret;
+ mode_t mode = 0444;
+
+ path = xasprintf("%s/%s", dir, file);
+ if ((fd = open(path, O_WRONLY|O_CREAT|O_EXCL, mode)) < 0) {
+ warn("%s", path);
+ free(path);
+ return (-1);
+ }
+ info("writing %s", path);
+ if ((f = fdopen(fd, "w")) == NULL)
+ err(1, NULL);
+ ret = 0;
+ RB_FOREACH(cert, cert_tree, tree) {
+ if (!PEM_write_X509(f, cert->x509)) {
+ warn("%s", cert->path);
+ ret = -1;
+ break;
+ }
+ }
+ if (mlf != NULL) {
+ fprintf(mlf,
+ "%s type=file mode=%#o size=%ld\n",
+ unexpand_path(path), mode, ftell(f));
+ }
+ fclose(f);
+ free(path);
+ 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.
+ */
+static int
+certctl_list(int argc, char **argv __unused)
+{
+ char *path;
+ int n;
+
+ if (argc > 1)
+ usage();
+ /* load trusted certificates */
+ path = expand_path(TRUSTED_PATH);
+ n = load_certs(path, &trusted, NULL);
+ free(path);
+ info("%d trusted certificates found", n);
+ /* list them */
+ list_certs(&trusted);
+ free_certs(&trusted);
+ return (0);
+}
+
+/*
+ * Load installed untrusted certificates, then list them.
+ */
+static int
+certctl_untrusted(int argc, char **argv __unused)
+{
+ char *path;
+ int n;
+
+ if (argc > 1)
+ usage();
+ /* load untrusted certificates */
+ path = expand_path(UNTRUSTED_PATH);
+ n = load_certs(path, &untrusted, NULL);
+ free(path);
+ /* ...including legacy ones */
+ path = expand_path(LEGACY_PATH);
+ n += load_certs(path, &untrusted, NULL);
+ free(path);
+ info("%d untrusted certificates found", n);
+ /* 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.
+ */
+static int
+certctl_rehash(int argc, char **argv __unused)
+{
+ char *path, *newpath, *oldpath;
+ unsigned int i, n;
+ int ret;
+
+ if (argc > 1)
+ usage();
+
+ if (unprivileged && (mlf = fopen(metalog, "a")) == NULL) {
+ warn("%s", metalog);
+ return (-1);
+ }
+
+ /* load untrusted certs first */
+ for (i = n = 0; i < nitems(untrusted_paths); i++) {
+ path = expand_path(untrusted_paths[i]);
+ n += load_certs(path, &untrusted, NULL);
+ free(path);
+ }
+ /* ...and manually untrusted certs */
+ path = expand_path(UNTRUSTED_PATH);
+ n += load_certs(UNTRUSTED_PATH, &untrusted, NULL);
+ free(path);
+ /* ...and legacy untrusted certs */
+ path = expand_path(LEGACY_PATH);
+ n += load_certs(LEGACY_PATH, &untrusted, NULL);
+ free(path);
+ info("%d untrusted certificates found", n);
+ /* load trusted certs, excluding any that are also untrusted */
+ for (i = n = 0; i < nitems(trusted_paths); i++) {
+ path = expand_path(trusted_paths[i]);
+ n += load_certs(path, &trusted, &untrusted);
+ free(path);
+ }
+ info("%d trusted certificates found", n);
+ /* create new directory */
+ path = expand_path(SSL_PATH);
+ if (!unprivileged) {
+ newpath = xasprintf("%s.new", path);
+ oldpath = xasprintf("%s.old", path);
+ if (rmpath(newpath) != 0) {
+ warn("failed to delete old %s", newpath);
+ ret = -1;
+ goto priv_end;
+ }
+ }
+ ret = 0;
+ /* wipe and save untrusted certs */
+ ret |= save_certs(unprivileged ? path : newpath, UNTRUSTED_SUBDIR, &untrusted);
+ /* wipe and save trusted certs */
+ ret |= save_certs(unprivileged ? path : newpath, TRUSTED_SUBDIR, &trusted);
+ /* save bundle */
+ ret |= save_bundle(unprivileged ? path : newpath, BUNDLE_FILE, &trusted);
+ /* rotate */
+ if (!unprivileged && ret == 0) {
+ if (rename(path, oldpath) != 0 && errno != ENOENT) {
+ warn("failed to rename %s to %s", path, oldpath);
+ if (rmpath(newpath) != 0)
+ warn("failed to delete %s", newpath);
+ ret = -1;
+ } else if (rename(newpath, path) != 0) {
+ warn("failed to rename %s to %s", newpath, path);
+ /* put it back! */
+ if (rename(oldpath, path) != 0)
+ warn("failed to rename %s back to %s", oldpath, path);
+ ret = -1;
+ } else {
+ if (rmpath(oldpath) != 0)
+ warn("failed to delete %s", oldpath);
+ }
+ free(oldpath);
+ free(newpath);
+ } else if (!unprivileged) {
+ if (rmpath(newpath) != 0)
+ warn("failed to delete %s", newpath);
+priv_end:
+ free(oldpath);
+ free(newpath);
+ }
+ /* clean up */
+ free(path);
+ free_certs(&untrusted);
+ free_certs(&trusted);
+ if (mlf != NULL)
+ fclose(mlf);
+ return (ret);
+}
+
+static void
+set_defaults(void)
+{
+ const char *value;
+ char *str;
+ size_t len;
+
+ if (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) {
+ if ((destdir = getenv("DESTDIR")) == NULL) {
+ destdir = "";
+ }
+ }
+ if (distbase == NULL) {
+ if ((distbase = getenv("DISTBASE")) == NULL) {
+ distbase = "";
+ }
+ }
+ if (unprivileged && metalog == NULL) {
+ if ((metalog = getenv("METALOG")) == NULL)
+ metalog = xasprintf("%s/METALOG", destdir);
+ }
+ if (!verbose) {
+ if ((value = getenv("CERTCTL_VERBOSE")) != NULL) {
+ if (value[0] != '\0') {
+ verbose = true;
+ }
+ }
+ }
+ info("localbase:\t%s", localbase);
+ info("destdir:\t%s", destdir);
+ info("distbase:\t%s", distbase);
+ 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 },
+};
+
+static void
+usage(void)
+{
+ fprintf(stderr, "usage: certctl [-lv] [-D destdir] [-d distbase] list\n"
+ " certctl [-lv] [-D destdir] [-d distbase] untrusted\n"
+ " certctl [-nUv] [-D destdir] [-d distbase] [-M metalog] rehash\n"
+ " certctl [-nv] [-D destdir] [-d distbase] untrust <file>\n"
+ " certctl [-nv] [-D destdir] [-d distbase] trust <file>\n");
+ exit(1);
+}
+
+int
+main(int argc, char *argv[])
+{
+ const char *command;
+ int opt;
+
+ while ((opt = getopt(argc, argv, "nD:d:lL:M:Uv")) != -1)
+ switch (opt) {
+ case 'D':
+ destdir = optarg;
+ break;
+ case 'd':
+ distbase = optarg;
+ break;
+ case 'l':
+ longnames = true;
+ break;
+ case 'L':
+ localbase = optarg;
+ break;
+ case 'M':
+ metalog = optarg;
+ break;
+ case 'n':
+ dryrun = true;
+ break;
+ case 'U':
+ unprivileged = true;
+ break;
+ case 'v':
+ verbose = true;
+ break;
+ default:
+ usage();
+ }
+
+ argc -= optind;
+ argv += optind;
+
+ if (argc < 1)
+ usage();
+
+ command = *argv;
+
+ if (unprivileged && strcmp(command, "rehash") != 0)
+ usage();
+ if (!unprivileged && metalog != NULL) {
+ warnx("-M may only be used in conjunction with -U");
+ usage();
+ }
+ if (dryrun)
+ errx(1, "-n is not yet implemented");
+
+ set_defaults();
+
+ for (unsigned i = 0; i < nitems(commands); i++)
+ if (strcmp(command, commands[i].name) == 0)
+ exit(!!commands[i].func(argc, argv));
+ usage();
+}

File Metadata

Mime Type
text/plain
Expires
Sat, Jun 20, 5:08 AM (4 h, 48 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
34111817
Default Alt Text
D42320.id129218.diff (17 KB)

Event Timeline