Page MenuHomeFreeBSD

D42320.id129178.diff
No OneTemporary

D42320.id129178.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.c b/usr.sbin/certctl/certctl.c
new file mode 100644
--- /dev/null
+++ b/usr.sbin/certctl/certctl.c
@@ -0,0 +1,563 @@
+/*-
+ * 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 bool dryrun;
+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",
+ NULL,
+};
+
+static const char *const untrusted_paths[] = {
+ "/usr/share/certs/untrusted",
+ "%L/etc/ssl/untrusted",
+ "%L/etc/ssl/blacklisted",
+ NULL,
+};
+
+#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 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));
+}
+
+static char **
+expand_paths(const char *const *paths)
+{
+ char **pathv;
+ size_t len;
+
+ for (len = 0; paths[len] != NULL; len++)
+ /* nothing */ ;
+ if ((pathv = calloc(len + 1, sizeof(*paths))) == NULL)
+ err(1, NULL);
+ for (unsigned int i = 0; i < len; i++)
+ pathv[i] = expand_path(paths[i]);
+ return (pathv);
+}
+
+static void
+free_paths(char **pathv)
+{
+ for (char **pathp = pathv; *pathp != NULL; pathp++)
+ free(*pathp);
+ free(pathv);
+}
+
+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 listed paths into a tree, optionally
+ * excluding those that already exist in a different tree.
+ */
+static int
+load_certs(const char *const *paths, struct cert_tree *tree,
+ struct cert_tree *exclude)
+{
+ FTS *fts;
+ FTSENT *ent;
+ FILE *f;
+ X509 *x509;
+ struct cert *cert;
+ unsigned long hash;
+ int fts_options;
+ int n, total = 0;
+
+ fts_options = FTS_LOGICAL | FTS_NOCHDIR;
+ if ((fts = fts_open((char *const *)(uintptr_t)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);
+ 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);
+}
+
+/*
+ * 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 void
+rmpath(const char *path)
+{
+ const char *paths[] = { path, NULL };
+ FTS *fts;
+ FTSENT *ent;
+ int fts_options;
+
+ fts_options = FTS_PHYSICAL | FTS_NOSTAT;
+ 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);
+ break;
+ }
+ }
+ fts_close(fts);
+}
+
+/*
+ * 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;
+
+ 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);
+ cert->path = malloc(len);
+ 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, 0444)) < 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 (unprivileged) {
+ fprintf(mlf,
+ "%s/%lx.%d type=file mode=0444 size=%ld\n",
+ TRUSTED_PATH, cert->hash, c, 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;
+
+ path = xasprintf("%s/%s", dir, file);
+ if ((fd = open(path, O_WRONLY|O_CREAT|O_EXCL, 0444)) < 0) {
+ warn("%s", path);
+ free(path);
+ return (-1);
+ }
+ 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;
+ }
+ }
+ fclose(f);
+ free(path);
+ return (ret);
+}
+
+/*
+ * Load trusted and untrusted certificates from all sources, then
+ * regenerate both the hashed directories and the bundle.
+ */
+static int
+certctl_rehash(void)
+{
+ char **paths;
+ char *path, *newpath, *oldpath;
+ int n, ret;
+
+ /* load untrusted certs first */
+ paths = expand_paths(untrusted_paths);
+ n = load_certs((const char * const *)paths, &untrusted, NULL);
+ n += load_certs((const char * const[]){ UNTRUSTED_PATH, NULL }, &untrusted, NULL);
+ info("%d untrusted certificates found", n);
+ free_paths(paths);
+ /* load trusted certs, excluding any that are also untrusted */
+ paths = expand_paths(trusted_paths);
+ n = load_certs((const char * const *)paths, &trusted, &untrusted);
+ info("%d trusted certificates found", n);
+ free_paths(paths);
+ /* create new directory */
+ path = expand_path(SSL_PATH);
+ if (!unprivileged) {
+ newpath = xasprintf("%s.new", path);
+ oldpath = xasprintf("%s.old", path);
+ rmpath(newpath);
+ }
+ 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);
+ rmpath(newpath);
+ exit(1);
+ }
+ 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);
+ exit(1);
+ }
+ rmpath(oldpath);
+ free(oldpath);
+ free(newpath);
+ } else if (!unprivileged) {
+ rmpath(newpath);
+ free(oldpath);
+ free(newpath);
+ }
+ /* clean up */
+ free(path);
+ free_certs(&untrusted);
+ free_certs(&trusted);
+ 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");
+}
+
+static void
+usage(void)
+{
+ fprintf(stderr, "usage: certctl [-v] [-D destdir] [-d distbase] list\n"
+ " certctl [-v] [-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[])
+{
+ int opt, ret;
+
+ while ((opt = getopt(argc, argv, "nD:d:L:M:Uv")) != -1)
+ switch (opt) {
+ case 'D':
+ destdir = optarg;
+ break;
+ case 'd':
+ distbase = optarg;
+ 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();
+ if (unprivileged && strcmp(argv[0], "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();
+
+ if (unprivileged && (mlf = fopen(metalog, "a")) == NULL)
+ err(1, "%s", metalog);
+
+ ret = -1;
+ if (argc == 1) {
+ if (strcmp(argv[0], "rehash") == 0) {
+ ret = certctl_rehash();
+ } else {
+ usage();
+ }
+ } else {
+ usage();
+ }
+ if (unprivileged)
+ fclose(mlf);
+ exit(ret == 0 ? 0 : 1);
+}

File Metadata

Mime Type
text/plain
Expires
Tue, Feb 3, 9:05 PM (5 h, 10 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
28426935
Default Alt Text
D42320.id129178.diff (12 KB)

Event Timeline