mirror of
https://git.freebsd.org/src.git
synced 2026-01-16 23:02:24 +00:00
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
1114 lines
24 KiB
C
1114 lines
24 KiB
C
/*-
|
|
* Copyright (c) 2023-2025 Dag-Erling Smørgrav <des@FreeBSD.org>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <sys/sysctl.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/tree.h>
|
|
|
|
#include <dirent.h>
|
|
#include <err.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <fts.h>
|
|
#include <paths.h>
|
|
#include <stdbool.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
|
|
#include <openssl/ssl.h>
|
|
|
|
#define info(fmt, ...) \
|
|
do { \
|
|
if (verbose) \
|
|
fprintf(stderr, fmt "\n", ##__VA_ARGS__); \
|
|
} while (0)
|
|
|
|
static char *
|
|
xasprintf(const char *fmt, ...)
|
|
{
|
|
va_list ap;
|
|
char *str;
|
|
int ret;
|
|
|
|
va_start(ap, fmt);
|
|
ret = vasprintf(&str, fmt, ap);
|
|
va_end(ap);
|
|
if (ret < 0 || str == NULL)
|
|
err(1, NULL);
|
|
return (str);
|
|
}
|
|
|
|
static char *
|
|
xstrdup(const char *str)
|
|
{
|
|
char *dup;
|
|
|
|
if ((dup = strdup(str)) == NULL)
|
|
err(1, NULL);
|
|
return (dup);
|
|
}
|
|
|
|
static void usage(void);
|
|
|
|
static bool dryrun;
|
|
static bool longnames;
|
|
static bool nobundle;
|
|
static bool unprivileged;
|
|
static bool verbose;
|
|
|
|
static const char *localbase;
|
|
static const char *destdir;
|
|
static const char *distbase;
|
|
static const char *metalog;
|
|
|
|
static const char *uname = "root";
|
|
static const char *gname = "wheel";
|
|
|
|
static const char *const default_trusted_paths[] = {
|
|
"/usr/share/certs/trusted",
|
|
"%L/share/certs/trusted",
|
|
"%L/share/certs",
|
|
NULL
|
|
};
|
|
static char **trusted_paths;
|
|
|
|
static const char *const default_untrusted_paths[] = {
|
|
"/usr/share/certs/untrusted",
|
|
"%L/share/certs/untrusted",
|
|
NULL
|
|
};
|
|
static char **untrusted_paths;
|
|
|
|
static char *trusted_dest;
|
|
static char *untrusted_dest;
|
|
static char *bundle_dest;
|
|
|
|
#define SSL_PATH "/etc/ssl"
|
|
#define TRUSTED_DIR "certs"
|
|
#define TRUSTED_PATH SSL_PATH "/" TRUSTED_DIR
|
|
#define UNTRUSTED_DIR "untrusted"
|
|
#define UNTRUSTED_PATH SSL_PATH "/" UNTRUSTED_DIR
|
|
#define LEGACY_DIR "blacklisted"
|
|
#define LEGACY_PATH SSL_PATH "/" LEGACY_DIR
|
|
#define BUNDLE_FILE "cert.pem"
|
|
#define BUNDLE_PATH SSL_PATH "/" BUNDLE_FILE
|
|
|
|
static FILE *mlf;
|
|
|
|
/*
|
|
* Remove duplicate and trailing slashes from a path.
|
|
*/
|
|
static char *
|
|
normalize_path(const char *str)
|
|
{
|
|
char *buf, *dst;
|
|
|
|
if ((buf = malloc(strlen(str) + 1)) == NULL)
|
|
err(1, NULL);
|
|
for (dst = buf; *str != '\0'; dst++) {
|
|
if ((*dst = *str++) == '/') {
|
|
while (*str == '/')
|
|
str++;
|
|
if (*str == '\0')
|
|
break;
|
|
}
|
|
}
|
|
*dst = '\0';
|
|
return (buf);
|
|
}
|
|
|
|
/*
|
|
* Split a colon-separated list into a NULL-terminated array.
|
|
*/
|
|
static char **
|
|
split_paths(const char *str)
|
|
{
|
|
char **paths;
|
|
const char *p, *q;
|
|
unsigned int i, n;
|
|
|
|
for (p = str, n = 1; *p; p++) {
|
|
if (*p == ':')
|
|
n++;
|
|
}
|
|
if ((paths = calloc(n + 1, sizeof(*paths))) == NULL)
|
|
err(1, NULL);
|
|
for (p = q = str, i = 0; i < n; i++, p = q + 1) {
|
|
q = strchrnul(p, ':');
|
|
if ((paths[i] = strndup(p, q - p)) == NULL)
|
|
err(1, NULL);
|
|
}
|
|
return (paths);
|
|
}
|
|
|
|
/*
|
|
* Expand %L into LOCALBASE and prefix DESTDIR and DISTBASE as needed.
|
|
*/
|
|
static char *
|
|
expand_path(const char *template)
|
|
{
|
|
if (template[0] == '%' && template[1] == 'L')
|
|
return (xasprintf("%s%s%s", destdir, localbase, template + 2));
|
|
return (xasprintf("%s%s%s", destdir, distbase, template));
|
|
}
|
|
|
|
/*
|
|
* Expand an array of paths.
|
|
*/
|
|
static char **
|
|
expand_paths(const char *const *templates)
|
|
{
|
|
char **paths;
|
|
unsigned int i, n;
|
|
|
|
for (n = 0; templates[n] != NULL; n++)
|
|
continue;
|
|
if ((paths = calloc(n + 1, sizeof(*paths))) == NULL)
|
|
err(1, NULL);
|
|
for (i = 0; i < n; i++)
|
|
paths[i] = expand_path(templates[i]);
|
|
return (paths);
|
|
}
|
|
|
|
/*
|
|
* If destdir is a prefix of path, returns a pointer to the rest of path,
|
|
* otherwise returns path.
|
|
*
|
|
* Note that this intentionally does not strip distbase from the path!
|
|
* Unlike destdir, distbase is expected to be included in the metalog.
|
|
*/
|
|
static const char *
|
|
unexpand_path(const char *path)
|
|
{
|
|
const char *p = path;
|
|
const char *q = destdir;
|
|
|
|
while (*p && *p == *q) {
|
|
p++;
|
|
q++;
|
|
}
|
|
return (*q == '\0' && *p == '/' ? p : path);
|
|
}
|
|
|
|
/*
|
|
* X509 certificate in a rank-balanced tree.
|
|
*/
|
|
struct cert {
|
|
RB_ENTRY(cert) entry;
|
|
unsigned long hash;
|
|
char *name;
|
|
X509 *x509;
|
|
char *path;
|
|
};
|
|
|
|
static void
|
|
free_cert(struct cert *cert)
|
|
{
|
|
free(cert->name);
|
|
X509_free(cert->x509);
|
|
free(cert->path);
|
|
free(cert);
|
|
}
|
|
|
|
static int
|
|
certcmp(const struct cert *a, const struct cert *b)
|
|
{
|
|
return (X509_cmp(a->x509, b->x509));
|
|
}
|
|
|
|
RB_HEAD(cert_tree, cert);
|
|
static struct cert_tree trusted = RB_INITIALIZER(&trusted);
|
|
static struct cert_tree untrusted = RB_INITIALIZER(&untrusted);
|
|
RB_GENERATE_STATIC(cert_tree, cert, entry, certcmp);
|
|
|
|
static void
|
|
free_certs(struct cert_tree *tree)
|
|
{
|
|
struct cert *cert, *tmp;
|
|
|
|
RB_FOREACH_SAFE(cert, cert_tree, tree, tmp) {
|
|
RB_REMOVE(cert_tree, tree, cert);
|
|
free_cert(cert);
|
|
}
|
|
}
|
|
|
|
static struct cert *
|
|
find_cert(struct cert_tree *haystack, X509 *x509)
|
|
{
|
|
struct cert needle = { .x509 = x509 };
|
|
|
|
return (RB_FIND(cert_tree, haystack, &needle));
|
|
}
|
|
|
|
/*
|
|
* File containing a certificate in a rank-balanced tree sorted by
|
|
* certificate hash and disambiguating counter. This is needed because
|
|
* the certificate hash function is prone to collisions, necessitating a
|
|
* counter to distinguish certificates that hash to the same value.
|
|
*/
|
|
struct file {
|
|
RB_ENTRY(file) entry;
|
|
const struct cert *cert;
|
|
unsigned int c;
|
|
};
|
|
|
|
static int
|
|
filecmp(const struct file *a, const struct file *b)
|
|
{
|
|
if (a->cert->hash > b->cert->hash)
|
|
return (1);
|
|
if (a->cert->hash < b->cert->hash)
|
|
return (-1);
|
|
return (a->c - b->c);
|
|
}
|
|
|
|
RB_HEAD(file_tree, file);
|
|
RB_GENERATE_STATIC(file_tree, file, entry, filecmp);
|
|
|
|
/*
|
|
* Lexicographical sort for scandir().
|
|
*/
|
|
static int
|
|
lexisort(const struct dirent **d1, const struct dirent **d2)
|
|
{
|
|
return (strcmp((*d1)->d_name, (*d2)->d_name));
|
|
}
|
|
|
|
/*
|
|
* Read certificate(s) from a single file and insert them into a tree.
|
|
* Ignore certificates that already exist in the tree. If exclude is not
|
|
* null, also ignore certificates that exist in exclude.
|
|
*
|
|
* Returns the number certificates added to the tree, or -1 on failure.
|
|
*/
|
|
static int
|
|
read_cert(const char *path, struct cert_tree *tree, struct cert_tree *exclude)
|
|
{
|
|
FILE *f;
|
|
X509 *x509;
|
|
X509_NAME *name;
|
|
struct cert *cert;
|
|
unsigned long hash;
|
|
int len, ni, no;
|
|
|
|
if ((f = fopen(path, "r")) == NULL) {
|
|
warn("%s", path);
|
|
return (-1);
|
|
}
|
|
for (ni = no = 0;
|
|
(x509 = PEM_read_X509(f, NULL, NULL, NULL)) != NULL;
|
|
ni++) {
|
|
hash = X509_subject_name_hash(x509);
|
|
if (exclude && find_cert(exclude, x509)) {
|
|
info("%08lx: excluded", hash);
|
|
X509_free(x509);
|
|
continue;
|
|
}
|
|
if (find_cert(tree, x509)) {
|
|
info("%08lx: duplicate", hash);
|
|
X509_free(x509);
|
|
continue;
|
|
}
|
|
if ((cert = calloc(1, sizeof(*cert))) == NULL)
|
|
err(1, NULL);
|
|
cert->x509 = x509;
|
|
name = X509_get_subject_name(x509);
|
|
cert->hash = X509_NAME_hash_ex(name, NULL, NULL, NULL);
|
|
len = X509_NAME_get_text_by_NID(name, NID_commonName,
|
|
NULL, 0);
|
|
if (len > 0) {
|
|
if ((cert->name = malloc(len + 1)) == NULL)
|
|
err(1, NULL);
|
|
X509_NAME_get_text_by_NID(name, NID_commonName,
|
|
cert->name, len + 1);
|
|
} else {
|
|
/* fallback for certificates without CN */
|
|
cert->name = X509_NAME_oneline(name, NULL, 0);
|
|
}
|
|
cert->path = xstrdup(unexpand_path(path));
|
|
if (RB_INSERT(cert_tree, tree, cert) != NULL)
|
|
errx(1, "unexpected duplicate");
|
|
info("%08lx: %s", cert->hash, cert->name);
|
|
no++;
|
|
}
|
|
/*
|
|
* ni is the number of certificates we found in the file.
|
|
* no is the number of certificates that weren't already in our
|
|
* tree or on the exclusion list.
|
|
*/
|
|
if (ni == 0)
|
|
warnx("%s: no valid certificates found", path);
|
|
fclose(f);
|
|
return (no);
|
|
}
|
|
|
|
/*
|
|
* Load all certificates found in the specified path into a tree,
|
|
* optionally excluding those that already exist in a different tree.
|
|
*
|
|
* Returns the number of certificates added to the tree, or -1 on failure.
|
|
*/
|
|
static int
|
|
read_certs(const char *path, struct cert_tree *tree, struct cert_tree *exclude)
|
|
{
|
|
struct stat sb;
|
|
char *paths[] = { (char *)(uintptr_t)path, NULL };
|
|
FTS *fts;
|
|
FTSENT *ent;
|
|
int fts_options = FTS_LOGICAL | FTS_NOCHDIR;
|
|
int ret, total = 0;
|
|
|
|
if (stat(path, &sb) != 0) {
|
|
return (-1);
|
|
} else if (!S_ISDIR(sb.st_mode)) {
|
|
errno = ENOTDIR;
|
|
return (-1);
|
|
}
|
|
if ((fts = fts_open(paths, fts_options, NULL)) == NULL)
|
|
err(1, "fts_open()");
|
|
while ((ent = fts_read(fts)) != NULL) {
|
|
if (ent->fts_info != FTS_F) {
|
|
if (ent->fts_info == FTS_ERR)
|
|
warnc(ent->fts_errno, "fts_read()");
|
|
continue;
|
|
}
|
|
info("found %s", ent->fts_path);
|
|
ret = read_cert(ent->fts_path, tree, exclude);
|
|
if (ret > 0)
|
|
total += ret;
|
|
}
|
|
fts_close(fts);
|
|
return (total);
|
|
}
|
|
|
|
/*
|
|
* Save the contents of a cert tree to disk.
|
|
*
|
|
* Returns 0 on success and -1 on failure.
|
|
*/
|
|
static int
|
|
write_certs(const char *dir, struct cert_tree *tree)
|
|
{
|
|
struct file_tree files = RB_INITIALIZER(&files);
|
|
struct cert *cert;
|
|
struct file *file, *tmp;
|
|
struct dirent **dents, **ent;
|
|
char *path, *tmppath = NULL;
|
|
FILE *f;
|
|
mode_t mode = 0444;
|
|
int cmp, d, fd, ndents, ret = 0;
|
|
|
|
/*
|
|
* Start by generating unambiguous file names for each certificate
|
|
* and storing them in lexicographical order
|
|
*/
|
|
RB_FOREACH(cert, cert_tree, tree) {
|
|
if ((file = calloc(1, sizeof(*file))) == NULL)
|
|
err(1, NULL);
|
|
file->cert = cert;
|
|
for (file->c = 0; file->c < INT_MAX; file->c++)
|
|
if (RB_INSERT(file_tree, &files, file) == NULL)
|
|
break;
|
|
if (file->c == INT_MAX)
|
|
errx(1, "unable to disambiguate %08lx", cert->hash);
|
|
free(cert->path);
|
|
cert->path = xasprintf("%08lx.%d", cert->hash, file->c);
|
|
}
|
|
/*
|
|
* Open and scan the directory.
|
|
*/
|
|
if ((d = open(dir, O_DIRECTORY | O_RDONLY)) < 0 ||
|
|
#ifdef BOOTSTRAPPING
|
|
(ndents = scandir(dir, &dents, NULL, lexisort))
|
|
#else
|
|
(ndents = fdscandir(d, &dents, NULL, lexisort))
|
|
#endif
|
|
< 0)
|
|
err(1, "%s", dir);
|
|
/*
|
|
* Iterate over the directory listing and the certificate listing
|
|
* in parallel. If the directory listing gets ahead of the
|
|
* certificate listing, we need to write the current certificate
|
|
* and advance the certificate listing. If the certificate
|
|
* listing is ahead of the directory listing, we need to delete
|
|
* the current file and advance the directory listing. If they
|
|
* are neck and neck, we have a match and could in theory compare
|
|
* the two, but in practice it's faster to just replace the
|
|
* current file with the current certificate (and advance both).
|
|
*/
|
|
ent = dents;
|
|
file = RB_MIN(file_tree, &files);
|
|
for (;;) {
|
|
if (ent < dents + ndents) {
|
|
/* skip directories */
|
|
if ((*ent)->d_type == DT_DIR) {
|
|
free(*ent++);
|
|
continue;
|
|
}
|
|
if (file != NULL) {
|
|
/* compare current dirent to current cert */
|
|
path = file->cert->path;
|
|
cmp = strcmp((*ent)->d_name, path);
|
|
} else {
|
|
/* trailing files in directory */
|
|
path = NULL;
|
|
cmp = -1;
|
|
}
|
|
} else {
|
|
if (file != NULL) {
|
|
/* trailing certificates */
|
|
path = file->cert->path;
|
|
cmp = 1;
|
|
} else {
|
|
/* end of both lists */
|
|
path = NULL;
|
|
break;
|
|
}
|
|
}
|
|
if (cmp < 0) {
|
|
/* a file on disk with no matching certificate */
|
|
info("removing %s/%s", dir, (*ent)->d_name);
|
|
if (!dryrun)
|
|
(void)unlinkat(d, (*ent)->d_name, 0);
|
|
free(*ent++);
|
|
continue;
|
|
}
|
|
if (cmp == 0) {
|
|
/* a file on disk with a matching certificate */
|
|
info("replacing %s/%s", dir, (*ent)->d_name);
|
|
if (dryrun) {
|
|
fd = open(_PATH_DEVNULL, O_WRONLY);
|
|
} else {
|
|
tmppath = xasprintf(".%s", path);
|
|
fd = openat(d, tmppath,
|
|
O_CREAT | O_WRONLY | O_TRUNC, mode);
|
|
if (!unprivileged && fd >= 0)
|
|
(void)fchmod(fd, mode);
|
|
}
|
|
free(*ent++);
|
|
} else {
|
|
/* a certificate with no matching file */
|
|
info("writing %s/%s", dir, path);
|
|
if (dryrun) {
|
|
fd = open(_PATH_DEVNULL, O_WRONLY);
|
|
} else {
|
|
tmppath = xasprintf(".%s", path);
|
|
fd = openat(d, tmppath,
|
|
O_CREAT | O_WRONLY | O_EXCL, mode);
|
|
}
|
|
}
|
|
/* write the certificate */
|
|
if (fd < 0 ||
|
|
(f = fdopen(fd, "w")) == NULL ||
|
|
!PEM_write_X509(f, file->cert->x509)) {
|
|
if (tmppath != NULL && fd >= 0) {
|
|
int serrno = errno;
|
|
(void)unlinkat(d, tmppath, 0);
|
|
errno = serrno;
|
|
}
|
|
err(1, "%s/%s", dir, tmppath ? tmppath : path);
|
|
}
|
|
/* rename temp file if applicable */
|
|
if (tmppath != NULL) {
|
|
if (ret == 0 && renameat(d, tmppath, d, path) != 0) {
|
|
warn("%s/%s", dir, path);
|
|
ret = -1;
|
|
}
|
|
if (ret != 0)
|
|
(void)unlinkat(d, tmppath, 0);
|
|
free(tmppath);
|
|
tmppath = NULL;
|
|
}
|
|
fflush(f);
|
|
/* emit metalog */
|
|
if (mlf != NULL) {
|
|
fprintf(mlf, ".%s/%s type=file "
|
|
"uname=%s gname=%s mode=%#o size=%ld\n",
|
|
unexpand_path(dir), path,
|
|
uname, gname, mode, ftell(f));
|
|
}
|
|
fclose(f);
|
|
/* advance certificate listing */
|
|
tmp = RB_NEXT(file_tree, &files, file);
|
|
RB_REMOVE(file_tree, &files, file);
|
|
free(file);
|
|
file = tmp;
|
|
}
|
|
free(dents);
|
|
close(d);
|
|
return (ret);
|
|
}
|
|
|
|
/*
|
|
* Save all certs in a tree to a single file (bundle).
|
|
*
|
|
* Returns 0 on success and -1 on failure.
|
|
*/
|
|
static int
|
|
write_bundle(const char *dir, const char *file, struct cert_tree *tree)
|
|
{
|
|
struct cert *cert;
|
|
char *tmpfile = NULL;
|
|
FILE *f;
|
|
int d, fd, ret = 0;
|
|
mode_t mode = 0444;
|
|
|
|
if (dir != NULL) {
|
|
if ((d = open(dir, O_DIRECTORY | O_RDONLY)) < 0)
|
|
err(1, "%s", dir);
|
|
} else {
|
|
dir = ".";
|
|
d = AT_FDCWD;
|
|
}
|
|
info("writing %s/%s", dir, file);
|
|
if (dryrun) {
|
|
fd = open(_PATH_DEVNULL, O_WRONLY);
|
|
} else {
|
|
tmpfile = xasprintf(".%s", file);
|
|
fd = openat(d, tmpfile, O_WRONLY | O_CREAT | O_EXCL, mode);
|
|
}
|
|
if (fd < 0 || (f = fdopen(fd, "w")) == NULL) {
|
|
if (tmpfile != NULL && fd >= 0) {
|
|
int serrno = errno;
|
|
(void)unlinkat(d, tmpfile, 0);
|
|
errno = serrno;
|
|
}
|
|
err(1, "%s/%s", dir, tmpfile ? tmpfile : file);
|
|
}
|
|
RB_FOREACH(cert, cert_tree, tree) {
|
|
if (!PEM_write_X509(f, cert->x509)) {
|
|
warn("%s/%s", dir, tmpfile ? tmpfile : file);
|
|
ret = -1;
|
|
break;
|
|
}
|
|
}
|
|
if (tmpfile != NULL) {
|
|
if (ret == 0 && renameat(d, tmpfile, d, file) != 0) {
|
|
warn("%s/%s", dir, file);
|
|
ret = -1;
|
|
}
|
|
if (ret != 0)
|
|
(void)unlinkat(d, tmpfile, 0);
|
|
free(tmpfile);
|
|
}
|
|
if (ret == 0 && mlf != NULL) {
|
|
fprintf(mlf,
|
|
".%s/%s type=file uname=%s gname=%s mode=%#o size=%ld\n",
|
|
unexpand_path(dir), file, uname, gname, mode, ftell(f));
|
|
}
|
|
fclose(f);
|
|
if (d != AT_FDCWD)
|
|
close(d);
|
|
return (ret);
|
|
}
|
|
|
|
/*
|
|
* Load trusted certificates.
|
|
*
|
|
* Returns the number of certificates loaded.
|
|
*/
|
|
static unsigned int
|
|
load_trusted(bool all, struct cert_tree *exclude)
|
|
{
|
|
unsigned int i, n;
|
|
int ret;
|
|
|
|
/* load external trusted certs */
|
|
for (i = n = 0; all && trusted_paths[i] != NULL; i++) {
|
|
ret = read_certs(trusted_paths[i], &trusted, exclude);
|
|
if (ret > 0)
|
|
n += ret;
|
|
}
|
|
|
|
/* 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);
|
|
}
|
|
|
|
/*
|
|
* Save everything.
|
|
*
|
|
* Returns 0 on success and -1 on failure.
|
|
*/
|
|
static int
|
|
save_all(void)
|
|
{
|
|
int ret = 0;
|
|
|
|
ret |= save_untrusted();
|
|
ret |= save_trusted();
|
|
if (!nobundle)
|
|
ret |= save_bundle();
|
|
return (ret);
|
|
}
|
|
|
|
/*
|
|
* List the contents of a certificate tree.
|
|
*/
|
|
static void
|
|
list_certs(struct cert_tree *tree)
|
|
{
|
|
struct cert *cert;
|
|
char *path, *name;
|
|
|
|
RB_FOREACH(cert, cert_tree, tree) {
|
|
path = longnames ? NULL : strrchr(cert->path, '/');
|
|
name = longnames ? NULL : strrchr(cert->name, '=');
|
|
printf("%s\t%s\n", path ? path + 1 : cert->path,
|
|
name ? name + 1 : cert->name);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Load installed trusted certificates, then list them.
|
|
*
|
|
* Returns 0 on success and -1 on failure.
|
|
*/
|
|
static int
|
|
certctl_list(int argc, char **argv __unused)
|
|
{
|
|
if (argc > 1)
|
|
usage();
|
|
/* load trusted certificates */
|
|
load_trusted(false, NULL);
|
|
/* list them */
|
|
list_certs(&trusted);
|
|
free_certs(&trusted);
|
|
return (0);
|
|
}
|
|
|
|
/*
|
|
* Load installed untrusted certificates, then list them.
|
|
*
|
|
* Returns 0 on success and -1 on failure.
|
|
*/
|
|
static int
|
|
certctl_untrusted(int argc, char **argv __unused)
|
|
{
|
|
if (argc > 1)
|
|
usage();
|
|
/* load untrusted certificates */
|
|
load_untrusted(false);
|
|
/* list them */
|
|
list_certs(&untrusted);
|
|
free_certs(&untrusted);
|
|
return (0);
|
|
}
|
|
|
|
/*
|
|
* Load trusted and untrusted certificates from all sources, then
|
|
* regenerate both the hashed directories and the bundle.
|
|
*
|
|
* Returns 0 on success and -1 on failure.
|
|
*/
|
|
static int
|
|
certctl_rehash(int argc, char **argv __unused)
|
|
{
|
|
int ret;
|
|
|
|
if (argc > 1)
|
|
usage();
|
|
|
|
if (unprivileged && (mlf = fopen(metalog, "a")) == NULL) {
|
|
warn("%s", metalog);
|
|
return (-1);
|
|
}
|
|
|
|
/* load untrusted certs first */
|
|
load_untrusted(true);
|
|
|
|
/* load trusted certs, excluding any that are already untrusted */
|
|
load_trusted(true, &untrusted);
|
|
|
|
/* save everything */
|
|
ret = save_all();
|
|
|
|
/* clean up */
|
|
free_certs(&untrusted);
|
|
free_certs(&trusted);
|
|
if (mlf != NULL)
|
|
fclose(mlf);
|
|
return (ret);
|
|
}
|
|
|
|
/*
|
|
* Manually add one or more certificates to the list of trusted certificates.
|
|
*
|
|
* Returns 0 on success and -1 on failure.
|
|
*/
|
|
static int
|
|
certctl_trust(int argc, char **argv)
|
|
{
|
|
struct cert_tree extra = RB_INITIALIZER(&extra);
|
|
struct cert *cert, *other, *tmp;
|
|
unsigned int n;
|
|
int i, ret;
|
|
|
|
if (argc < 2)
|
|
usage();
|
|
|
|
/* load untrusted certs first */
|
|
load_untrusted(true);
|
|
|
|
/* load trusted certs, excluding any that are already untrusted */
|
|
load_trusted(true, &untrusted);
|
|
|
|
/* now load the additional trusted certificates */
|
|
n = 0;
|
|
for (i = 1; i < argc; i++) {
|
|
ret = read_cert(argv[i], &extra, &trusted);
|
|
if (ret > 0)
|
|
n += ret;
|
|
}
|
|
if (n == 0) {
|
|
warnx("no new trusted certificates found");
|
|
free_certs(&untrusted);
|
|
free_certs(&trusted);
|
|
free_certs(&extra);
|
|
return (0);
|
|
}
|
|
|
|
/*
|
|
* For each new trusted cert, move it from the extra list to the
|
|
* trusted list, then check if a matching certificate exists on
|
|
* the untrusted list. If that is the case, warn the user, then
|
|
* remove the matching certificate from the untrusted list.
|
|
*/
|
|
RB_FOREACH_SAFE(cert, cert_tree, &extra, tmp) {
|
|
RB_REMOVE(cert_tree, &extra, cert);
|
|
RB_INSERT(cert_tree, &trusted, cert);
|
|
if ((other = RB_FIND(cert_tree, &untrusted, cert)) != NULL) {
|
|
warnx("%s was previously untrusted", cert->name);
|
|
RB_REMOVE(cert_tree, &untrusted, other);
|
|
free_cert(other);
|
|
}
|
|
}
|
|
|
|
/* save everything */
|
|
ret = save_all();
|
|
|
|
/* clean up */
|
|
free_certs(&untrusted);
|
|
free_certs(&trusted);
|
|
return (ret);
|
|
}
|
|
|
|
/*
|
|
* Manually add one or more certificates to the list of untrusted
|
|
* certificates.
|
|
*
|
|
* Returns 0 on success and -1 on failure.
|
|
*/
|
|
static int
|
|
certctl_untrust(int argc, char **argv)
|
|
{
|
|
unsigned int n;
|
|
int i, ret;
|
|
|
|
if (argc < 2)
|
|
usage();
|
|
|
|
/* load untrusted certs first */
|
|
load_untrusted(true);
|
|
|
|
/* now load the additional untrusted certificates */
|
|
n = 0;
|
|
for (i = 1; i < argc; i++) {
|
|
ret = read_cert(argv[i], &untrusted, NULL);
|
|
if (ret > 0)
|
|
n += ret;
|
|
}
|
|
if (n == 0) {
|
|
warnx("no new untrusted certificates found");
|
|
free_certs(&untrusted);
|
|
return (0);
|
|
}
|
|
|
|
/* load trusted certs, excluding any that are already untrusted */
|
|
load_trusted(true, &untrusted);
|
|
|
|
/* save everything */
|
|
ret = save_all();
|
|
|
|
/* clean up */
|
|
free_certs(&untrusted);
|
|
free_certs(&trusted);
|
|
return (ret);
|
|
}
|
|
|
|
static void
|
|
set_defaults(void)
|
|
{
|
|
const char *value;
|
|
char *str;
|
|
size_t len;
|
|
|
|
if (localbase == NULL &&
|
|
(localbase = getenv("LOCALBASE")) == NULL) {
|
|
if ((str = malloc((len = PATH_MAX) + 1)) == NULL)
|
|
err(1, NULL);
|
|
while (sysctlbyname("user.localbase", str, &len, NULL, 0) < 0) {
|
|
if (errno != ENOMEM)
|
|
err(1, "sysctl(user.localbase)");
|
|
if ((str = realloc(str, len + 1)) == NULL)
|
|
err(1, NULL);
|
|
}
|
|
str[len] = '\0';
|
|
localbase = str;
|
|
}
|
|
|
|
if (destdir == NULL &&
|
|
(destdir = getenv("DESTDIR")) == NULL)
|
|
destdir = "";
|
|
destdir = normalize_path(destdir);
|
|
|
|
if (distbase == NULL &&
|
|
(distbase = getenv("DISTBASE")) == NULL)
|
|
distbase = "";
|
|
if (*distbase != '\0' && *distbase != '/')
|
|
errx(1, "DISTBASE=%s does not begin with a slash", distbase);
|
|
distbase = normalize_path(distbase);
|
|
|
|
if (unprivileged && metalog == NULL &&
|
|
(metalog = getenv("METALOG")) == NULL)
|
|
metalog = xasprintf("%s/METALOG", destdir);
|
|
|
|
if (!verbose) {
|
|
if ((value = getenv("CERTCTL_VERBOSE")) != NULL) {
|
|
if (value[0] != '\0') {
|
|
verbose = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ((value = getenv("TRUSTPATH")) != NULL)
|
|
trusted_paths = split_paths(value);
|
|
else
|
|
trusted_paths = expand_paths(default_trusted_paths);
|
|
|
|
if ((value = getenv("UNTRUSTPATH")) != NULL)
|
|
untrusted_paths = split_paths(value);
|
|
else
|
|
untrusted_paths = expand_paths(default_untrusted_paths);
|
|
|
|
if ((value = getenv("TRUSTDESTDIR")) != NULL ||
|
|
(value = getenv("CERTDESTDIR")) != NULL)
|
|
trusted_dest = xstrdup(value);
|
|
else
|
|
trusted_dest = expand_path(TRUSTED_PATH);
|
|
|
|
if ((value = getenv("UNTRUSTDESTDIR")) != NULL)
|
|
untrusted_dest = xstrdup(value);
|
|
else
|
|
untrusted_dest = expand_path(UNTRUSTED_PATH);
|
|
|
|
if ((value = getenv("BUNDLE")) != NULL)
|
|
bundle_dest = xstrdup(value);
|
|
else
|
|
bundle_dest = expand_path(BUNDLE_PATH);
|
|
|
|
info("localbase:\t%s", localbase);
|
|
info("destdir:\t%s", destdir);
|
|
info("distbase:\t%s", distbase);
|
|
info("unprivileged:\t%s", unprivileged ? "true" : "false");
|
|
info("verbose:\t%s", verbose ? "true" : "false");
|
|
}
|
|
|
|
typedef int (*main_t)(int, char **);
|
|
|
|
static struct {
|
|
const char *name;
|
|
main_t func;
|
|
} commands[] = {
|
|
{ "list", certctl_list },
|
|
{ "untrusted", certctl_untrusted },
|
|
{ "rehash", certctl_rehash },
|
|
{ "untrust", certctl_untrust },
|
|
{ "trust", certctl_trust },
|
|
{ 0 },
|
|
};
|
|
|
|
static void
|
|
usage(void)
|
|
{
|
|
fprintf(stderr, "usage: certctl [-lv] [-D destdir] [-d distbase] list\n"
|
|
" certctl [-lv] [-D destdir] [-d distbase] untrusted\n"
|
|
" certctl [-BnUv] [-D destdir] [-d distbase] [-M metalog] rehash\n"
|
|
" certctl [-nv] [-D destdir] [-d distbase] untrust <file>\n"
|
|
" certctl [-nv] [-D destdir] [-d distbase] trust <file>\n");
|
|
exit(1);
|
|
}
|
|
|
|
int
|
|
main(int argc, char *argv[])
|
|
{
|
|
const char *command;
|
|
int opt;
|
|
|
|
while ((opt = getopt(argc, argv, "BcD:d:g:lL:M:no:Uv")) != -1)
|
|
switch (opt) {
|
|
case 'B':
|
|
nobundle = true;
|
|
break;
|
|
case 'c':
|
|
/* ignored for compatibility */
|
|
break;
|
|
case 'D':
|
|
destdir = optarg;
|
|
break;
|
|
case 'd':
|
|
distbase = optarg;
|
|
break;
|
|
case 'g':
|
|
gname = optarg;
|
|
break;
|
|
case 'l':
|
|
longnames = true;
|
|
break;
|
|
case 'L':
|
|
localbase = optarg;
|
|
break;
|
|
case 'M':
|
|
metalog = optarg;
|
|
break;
|
|
case 'n':
|
|
dryrun = true;
|
|
break;
|
|
case 'o':
|
|
uname = optarg;
|
|
break;
|
|
case 'U':
|
|
unprivileged = true;
|
|
break;
|
|
case 'v':
|
|
verbose = true;
|
|
break;
|
|
default:
|
|
usage();
|
|
}
|
|
|
|
argc -= optind;
|
|
argv += optind;
|
|
|
|
if (argc < 1)
|
|
usage();
|
|
|
|
command = *argv;
|
|
|
|
if ((nobundle || unprivileged || metalog != NULL) &&
|
|
strcmp(command, "rehash") != 0)
|
|
usage();
|
|
if (!unprivileged && metalog != NULL) {
|
|
warnx("-M may only be used in conjunction with -U");
|
|
usage();
|
|
}
|
|
|
|
set_defaults();
|
|
|
|
for (unsigned i = 0; commands[i].name != NULL; i++)
|
|
if (strcmp(command, commands[i].name) == 0)
|
|
exit(!!commands[i].func(argc, argv));
|
|
usage();
|
|
}
|