Page Menu
Home
FreeBSD
Search
Configure Global Search
Log In
Files
F143978189
D42320.id129178.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
12 KB
Referenced Files
None
Subscribers
None
D42320.id129178.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D42320: certctl: Reimplement in C
Attached
Detach File
Event Timeline
Log In to Comment