On 13 Aug 2025, at 23:25, Dag-Erling Smørgrav <d...@freebsd.org> wrote:
> 
> 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.

That’s not quite true. DISTBASE was separate from DESTDIR because the
expectation of distributeworld is that there is a single METALOG file
for all of the distribution sets combined, where each line in the
METALOG includes the distribution set’s directory name. Have you
verified that distributeworld -DNO_ROOT (as is now the only supported
option for release builds) works correctly and includes all the hashed
certs?

See 232cf6be4bc4 ("certctl: Introduce a new -d <distbase> option”) for
the rationale behind why I introduced it as a separate option in the
first place; prior to that there was just DESTDIR that pointed at the
distribution’s subdirectory.

As mentioned on IRC this also breaks the macOS cross-build due to not
being able to find OpenSSL headers.

Jessica

>    * 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