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