git: c340ef28fd38 - main - certctl: Reimplement in C
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
Date: Mon, 18 Aug 2025 14:35:19 UTC
The branch main has been updated by des:
URL: https://cgit.FreeBSD.org/src/commit/?id=c340ef28fd384b567e35882d04ce17fa31b7384f
commit c340ef28fd384b567e35882d04ce17fa31b7384f
Author: Dag-Erling Smørgrav <des@FreeBSD.org>
AuthorDate: 2025-08-18 14:26:29 +0000
Commit: Dag-Erling Smørgrav <des@FreeBSD.org>
CommitDate: 2025-08-18 14:28:29 +0000
certctl: Reimplement in C
Notable changes include:
* We no longer forget manually untrusted certificates when rehashing.
* Rehash will now scan the existing directory and progressively replace
its contents with those of the new trust store. The trust store as a
whole is not replaced atomically, but each file within it is.
* We no longer attempt to link to the original files, but we don't copy
them either. Instead, we write each certificate out in its minimal
form.
* We now generate a trust bundle in addition to the hashed diretory.
This also contains only the minimal DER form of each certificate.
This allows e.g. Unbound to preload the bundle before chrooting.
* The C version is approximately two orders of magnitude faster than the
sh version, with rehash taking ~100 ms vs ~5-25 s depending on whether
ca_root_nss is installed.
* We now also have tests.
Reviewed by: kevans, markj
Differential Revision: https://reviews.freebsd.org/D42320
Differential Revision: https://reviews.freebsd.org/D51896
---
Makefile.inc1 | 18 +-
etc/mtree/BSD.tests.dist | 2 +
share/man/man7/hier.7 | 17 +-
sys/sys/param.h | 2 +-
usr.sbin/certctl/Makefile | 11 +-
usr.sbin/certctl/certctl.8 | 96 ++-
usr.sbin/certctl/certctl.c | 1114 ++++++++++++++++++++++++++++++++
usr.sbin/certctl/certctl.sh | 366 -----------
usr.sbin/certctl/tests/Makefile | 5 +
usr.sbin/certctl/tests/certctl.subr | 44 ++
usr.sbin/certctl/tests/certctl_test.sh | 332 ++++++++++
11 files changed, 1596 insertions(+), 411 deletions(-)
diff --git a/Makefile.inc1 b/Makefile.inc1
index d8853fef321b..a16af09caea0 100644
--- a/Makefile.inc1
+++ b/Makefile.inc1
@@ -1542,14 +1542,10 @@ distributeworld installworld stageworld: _installcheck_world .PHONY
.endif # make(distributeworld)
${_+_}cd ${.CURDIR}; ${IMAKE} re${.TARGET:S/world$//}; \
${IMAKEENV} rm -rf ${INSTALLTMP}
-.if !make(packageworld) && ${MK_CAROOT} != "no"
- @if which openssl>/dev/null; then \
- PATH=${TMPPATH:Q}:${PATH:Q} \
- LOCALBASE=${LOCALBASE:Q} \
- sh ${SRCTOP}/usr.sbin/certctl/certctl.sh ${CERTCTLFLAGS} rehash; \
- else \
- echo "No openssl on the host, not rehashing certificates target -- /etc/ssl may not be populated."; \
- fi
+.if !make(packageworld) && ${MK_CAROOT} != "no" && ${MK_OPENSSL} != "no"
+ PATH=${TMPPATH:Q}:${PATH:Q} \
+ LOCALBASE=${LOCALBASE:Q} \
+ certctl ${CERTCTLFLAGS} rehash
.endif
.if make(distributeworld)
.for dist in ${EXTRA_DISTRIBUTIONS}
@@ -2713,6 +2709,11 @@ _basic_bootstrap_tools+=sbin/md5
_basic_bootstrap_tools+=usr.sbin/tzsetup
.endif
+# certctl is needed as an install tool
+.if ${MK_CAROOT} != "no" && ${MK_OPENSSL} != "no"
+_certctl=usr.sbin/certctl
+.endif
+
.if defined(BOOTSTRAP_ALL_TOOLS)
_other_bootstrap_tools+=${_basic_bootstrap_tools}
.for _subdir _links in ${_basic_bootstrap_tools_multilink}
@@ -2776,6 +2777,7 @@ bootstrap-tools: ${_bt}-links .PHONY
${_strfile} \
usr.bin/dtc \
${_cat} \
+ ${_certctl} \
${_kbdcontrol} \
${_elftoolchain_libs} \
${_libkldelf} \
diff --git a/etc/mtree/BSD.tests.dist b/etc/mtree/BSD.tests.dist
index 2c25d9386032..e6a013f010de 100644
--- a/etc/mtree/BSD.tests.dist
+++ b/etc/mtree/BSD.tests.dist
@@ -1255,6 +1255,8 @@
..
..
usr.sbin
+ certctl
+ ..
chown
..
ctladm
diff --git a/share/man/man7/hier.7 b/share/man/man7/hier.7
index 1c69b911f53b..814f5b769be8 100644
--- a/share/man/man7/hier.7
+++ b/share/man/man7/hier.7
@@ -28,7 +28,7 @@
.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
.\" SUCH DAMAGE.
.\"
-.Dd October 10, 2024
+.Dd August 18, 2025
.Dt HIER 7
.Os
.Sh NAME
@@ -308,6 +308,21 @@ OpenSSH configuration files; see
.Xr ssh 1
.It Pa ssl/
OpenSSL configuration files
+.Pp
+.Bl -tag -width "untrusted/" -compact
+.It Pa cert.pem
+System trust store in bundle form; see
+.Xr certctl 8 .
+.It Pa certs/
+System trust store in OpenSSL hashed-directory form; see
+.Xr certctl 8 .
+.It Pa openssl.cnf
+OpenSSL configuration file; see
+.Xr openssl.cnf 5 .
+.It Pa untrusted/
+Explicitly distrusted certificates; see
+.Xr certctl 8 .
+.El
.It Pa sysctl.conf
kernel state defaults; see
.Xr sysctl.conf 5
diff --git a/sys/sys/param.h b/sys/sys/param.h
index 915bfe1abfcd..fc2a78883f1e 100644
--- a/sys/sys/param.h
+++ b/sys/sys/param.h
@@ -74,7 +74,7 @@
* cannot include sys/param.h and should only be updated here.
*/
#undef __FreeBSD_version
-#define __FreeBSD_version 1500062
+#define __FreeBSD_version 1500063
/*
* __FreeBSD_kernel__ indicates that this system uses the kernel of FreeBSD,
diff --git a/usr.sbin/certctl/Makefile b/usr.sbin/certctl/Makefile
index 88c024daf7e6..6900f0ce3b65 100644
--- a/usr.sbin/certctl/Makefile
+++ b/usr.sbin/certctl/Makefile
@@ -1,5 +1,14 @@
+.include <src.opts.mk>
+
PACKAGE= certctl
-SCRIPTS=certctl.sh
+PROG= certctl
MAN= certctl.8
+LIBADD= crypto
+HAS_TESTS=
+SUBDIR.${MK_TESTS}= tests
+
+.ifdef BOOTSTRAPPING
+CFLAGS+=-DBOOTSTRAPPING
+.endif
.include <bsd.prog.mk>
diff --git a/usr.sbin/certctl/certctl.8 b/usr.sbin/certctl/certctl.8
index 7e49bb89e2ac..edf993e1361a 100644
--- 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 17, 2025
+.Dd August 18, 2025
.Dt CERTCTL 8
.Os
.Sh NAME
@@ -32,63 +32,85 @@
.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 cnUv
+.Op Fl BnUv
.Op Fl D Ar destdir
.Op Fl M Ar metalog
.Ic rehash
.Nm
-.Op Fl cnv
-.Ic untrust Ar file
+.Op Fl nv
+.Ic untrust Ar
.Nm
-.Op Fl cnv
-.Ic trust Ar file
+.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
-Flags:
+The following options are available:
.Bl -tag -width 4n
-.It Fl c
-Copy certificates instead of linking to them.
+.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 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).
+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
-No-Op mode, do not actually perform any actions.
+Dry-run mode.
+Do not actually perform any actions except write the metalog.
.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.
+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 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
@@ -97,9 +119,13 @@ Remove the specified file from the untrusted list.
.Sh ENVIRONMENT
.Bl -tag -width UNTRUSTDESTDIR
.It Ev DESTDIR
-Alternate destination directory to operate on.
+Absolute path to an alternate destination directory to operate on
+instead of the file system root, e.g.
+.Dq Li /tmp/install .
.It Ev DISTBASE
Additional path component to include when operating on certificate directories.
+This must start with a slash, e.g.
+.Dq Li /base .
.It Ev LOCALBASE
Location for local programs.
Defaults to the value of the user.localbase sysctl which is usually
@@ -107,32 +133,34 @@ Defaults to the value of the user.localbase sysctl which is usually
.It Ev TRUSTPATH
List of paths to search for trusted certificates.
Default:
-.Pa <DESTDIR><DISTBASE>/usr/share/certs/trusted
-.Pa <DESTDIR><DISTBASE>/usr/local/share/certs
-.Pa <DESTDIR><DISTBASE><LOCALBASE>/etc/ssl/certs
+.Pa ${DESTDIR}${DISTBASE}/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><DISTBASE>/usr/share/certs/untrusted
-.Pa <DESTDIR><DISTBASE><LOCALBASE>/etc/ssl/untrusted
-.Pa <DESTDIR><DISTBASE><LOCALBASE>/etc/ssl/blacklisted
-.It Ev CERTDESTDIR
+.Pa ${DESTDIR}${DISTBASE}/usr/share/certs/untrusted
+.Pa ${DESTDIR}${LOCALBASE}/share/certs/untrusted
+.It Ev TRUSTDESTDIR
Destination directory for symbolic links to trusted certificates.
Default:
-.Pa <DESTDIR><DISTBASE>/etc/ssl/certs
+.Pa ${DESTDIR}${DISTBASE}/etc/ssl/certs
.It Ev UNTRUSTDESTDIR
Destination directory for symbolic links to untrusted certificates.
Default:
-.Pa <DESTDIR><DISTBASE>/etc/ssl/untrusted
-.It Ev EXTENSIONS
-List of file extensions to read as certificate files.
-Default: *.pem *.crt *.cer *.crl *.0
+.Pa ${DESTDIR}${DISTBASE}/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
+.Fx 12.2 .
.Sh AUTHORS
-.An Allan Jude Aq Mt allanjude@freebsd.org
+.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
new file mode 100644
index 000000000000..ed7f05126ca7
--- /dev/null
+++ b/usr.sbin/certctl/certctl.c
@@ -0,0 +1,1114 @@
+/*-
+ * Copyright (c) 2023-2025 Dag-Erling Smørgrav <des@FreeBSD.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <sys/sysctl.h>
+#include <sys/stat.h>
+#include <sys/tree.h>
+
+#include <dirent.h>
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <fts.h>
+#include <paths.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 nobundle;
+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 *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;
+
+/*
+ * Remove duplicate and trailing slashes from a path.
+ */
+static char *
+normalize_path(const char *str)
+{
+ char *buf, *dst;
+
+ if ((buf = malloc(strlen(str) + 1)) == NULL)
+ err(1, NULL);
+ for (dst = buf; *str != '\0'; dst++) {
+ if ((*dst = *str++) == '/') {
+ while (*str == '/')
+ str++;
+ if (*str == '\0')
+ break;
+ }
+ }
+ *dst = '\0';
+ return (buf);
+}
+
+/*
+ * 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 and DISTBASE as needed.
+ */
+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%s", destdir, distbase, 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.
+ *
+ * Note that this intentionally does not strip distbase from the path!
+ * Unlike destdir, distbase is expected to be included in the metalog.
+ */
+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 len, 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);
+ len = X509_NAME_get_text_by_NID(name, NID_commonName,
+ NULL, 0);
+ if (len > 0) {
+ if ((cert->name = malloc(len + 1)) == NULL)
+ err(1, NULL);
+ X509_NAME_get_text_by_NID(name, NID_commonName,
+ cert->name, len + 1);
+ } else {
+ /* fallback for certificates without CN */
+ 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, cert->name);
+ 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);
+ 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)
+ 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;
+ }
+ fflush(f);
+ /* 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;
*** 1260 LINES SKIPPED ***