The branch main has been updated by des:

URL: 
https://cgit.FreeBSD.org/src/commit/?id=81d8827ad8752e35411204541f1f09df1481e417

commit 81d8827ad8752e35411204541f1f09df1481e417
Author:     Dag-Erling Smørgrav <d...@freebsd.org>
AuthorDate: 2025-08-13 22:25:27 +0000
Commit:     Dag-Erling Smørgrav <d...@freebsd.org>
CommitDate: 2025-08-13 22:25:27 +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.
    
    * 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.
    
    * The DISTBASE concept has been dropped; the same effect can be achieved
      by adjusting DESTDIR.
    
    * We now also have rudimentary tests.
    
    Reviewed by:    kevans
    Differential Revision:  https://reviews.freebsd.org/D42320
---
 Makefile.inc1                          |   21 +-
 usr.sbin/certctl/Makefile              |    7 +-
 usr.sbin/certctl/certctl.8             |   94 +--
 usr.sbin/certctl/certctl.c             | 1060 ++++++++++++++++++++++++++++++++
 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 |  221 +++++++
 8 files changed, 1404 insertions(+), 414 deletions(-)

diff --git a/Makefile.inc1 b/Makefile.inc1
index 9128d1d8ee77..e67bc7f5d1b1 100644
--- a/Makefile.inc1
+++ b/Makefile.inc1
@@ -1021,8 +1021,7 @@ IMAKE_MTREE=      MTREE_CMD="${MTREE_CMD} ${MTREEFLAGS}"
 .endif
 
 .if make(distributeworld)
-CERTCTLDESTDIR=        ${DESTDIR}/${DISTDIR}
-CERTCTLFLAGS+= -d /base
+CERTCTLDESTDIR=        ${DESTDIR}/${DISTDIR}/base
 .else
 CERTCTLDESTDIR=        ${DESTDIR}
 .endif
@@ -1541,14 +1540,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}
@@ -2712,6 +2707,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}
@@ -2775,6 +2775,7 @@ bootstrap-tools: ${_bt}-links .PHONY
     ${_strfile} \
     usr.bin/dtc \
     ${_cat} \
+    ${_certctl} \
     ${_kbdcontrol} \
     ${_elftoolchain_libs} \
     ${_libkldelf} \
diff --git a/usr.sbin/certctl/Makefile b/usr.sbin/certctl/Makefile
index 88c024daf7e6..5430dbf24853 100644
--- a/usr.sbin/certctl/Makefile
+++ b/usr.sbin/certctl/Makefile
@@ -1,5 +1,10 @@
+.include <src.opts.mk>
+
 PACKAGE=       certctl
-SCRIPTS=certctl.sh
+PROG=  certctl
 MAN=   certctl.8
+LIBADD=        crypto
+HAS_TESTS=
+SUBDIR.${MK_TESTS}=    tests
 
 .include <bsd.prog.mk>
diff --git a/usr.sbin/certctl/certctl.8 b/usr.sbin/certctl/certctl.8
index 7e49bb89e2ac..97bdc840c359 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 11, 2025
 .Dt CERTCTL 8
 .Os
 .Sh NAME
@@ -32,63 +32,83 @@
 .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
@@ -98,8 +118,6 @@ Remove the specified file from the untrusted list.
 .Bl -tag -width UNTRUSTDESTDIR
 .It Ev DESTDIR
 Alternate destination directory to operate on.
-.It Ev DISTBASE
-Additional path component to include when operating on certificate directories.
 .It Ev LOCALBASE
 Location for local programs.
 Defaults to the value of the user.localbase sysctl which is usually
@@ -107,32 +125,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}/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}/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}/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}/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 allanj...@freebsd.org
+.An -nosplit
+The original shell implementation was written by
+.An Allan Jude Aq Mt allanj...@freebsd.org .
+The current C implementation was written by
+.An Dag-Erling Sm\(/orgrav Aq Mt d...@freebsd.org .
diff --git a/usr.sbin/certctl/certctl.c b/usr.sbin/certctl/certctl.c
new file mode 100644
index 000000000000..6687e56f23b4
--- /dev/null
+++ b/usr.sbin/certctl/certctl.c
@@ -0,0 +1,1060 @@
+/*-
+ * Copyright (c) 2023-2025 Dag-Erling Smørgrav <d...@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 *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;
+
+/*
+ * 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.
+ */
+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", destdir, 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.
+ */
+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 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);
+               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, strrchr(cert->name, '=') + 1);
+               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 ||
+           (ndents = fdscandir(d, &dents, NULL, lexisort)) < 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;
+               }
+               /* 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;
+       }
+
+       /* load installed trusted certs */
+       ret = read_certs(trusted_dest, &trusted, exclude);
+       if (ret > 0)
+               n += ret;
+
+       info("%d trusted certificates found", n);
+       return (n);
+}
+
+/*
+ * Load untrusted certificates.
+ *
+ * Returns the number of certificates loaded.
+ */
+static unsigned int
+load_untrusted(bool all)
+{
+       char *path;
+       unsigned int i, n;
+       int ret;
+
+       /* load external untrusted certs */
+       for (i = n = 0; all && untrusted_paths[i] != NULL; i++) {
+               ret = read_certs(untrusted_paths[i], &untrusted, NULL);
+               if (ret > 0)
+                       n += ret;
+       }
+
+       /* load installed untrusted certs */
+       ret = read_certs(untrusted_dest, &untrusted, NULL);
+       if (ret > 0)
+               n += ret;
+
+       /* load legacy untrusted certs */
+       path = expand_path(LEGACY_PATH);
+       ret = read_certs(path, &untrusted, NULL);
+       if (ret > 0) {
+               warnx("certificates found in legacy directory %s",
+                   path);
+               n += ret;
+       } else if (ret == 0) {
+               warnx("legacy directory %s can safely be deleted",
+                   path);
+       }
+       free(path);
+
+       info("%d untrusted certificates found", n);
+       return (n);
+}
+
+/*
+ * Save trusted certificates.
+ *
+ * Returns 0 on success and -1 on failure.
+ */
+static int
+save_trusted(void)
+{
+       int ret;
+
+       /* save untrusted certs */
+       ret = write_certs(trusted_dest, &trusted);
+       return (ret);
+}
+
+/*
+ * Save untrusted certificates.
+ *
+ * Returns 0 on success and -1 on failure.
+ */
+static int
+save_untrusted(void)
+{
+       int ret;
+
+       ret = write_certs(untrusted_dest, &untrusted);
+       return (ret);
+}
+
+/*
+ * Save certificate bundle.
+ *
+ * Returns 0 on success and -1 on failure.
+ */
+static int
+save_bundle(void)
+{
+       char *dir, *file, *sep;
+       int ret;
+
+       if ((sep = strrchr(bundle_dest, '/')) == NULL) {
+               dir = NULL;
+               file = bundle_dest;
+       } else {
+               dir = xasprintf("%.*s", (int)(sep - bundle_dest), bundle_dest);
+               file = sep + 1;
+       }
+       ret = write_bundle(dir, file, &trusted);
+       free(dir);
+       return (ret);
+}
+
+/*
*** 1032 LINES SKIPPED ***

Reply via email to