The branch main has been updated by des:

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

commit c340ef28fd384b567e35882d04ce17fa31b7384f
Author:     Dag-Erling Smørgrav <d...@freebsd.org>
AuthorDate: 2025-08-18 14:26:29 +0000
Commit:     Dag-Erling Smørgrav <d...@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 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..ed7f05126ca7
--- /dev/null
+++ b/usr.sbin/certctl/certctl.c
@@ -0,0 +1,1114 @@
+/*-
+ * 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 *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 ***

Reply via email to