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