aboutsummaryrefslogtreecommitdiff
path: root/usr.sbin
diff options
context:
space:
mode:
Diffstat (limited to 'usr.sbin')
-rw-r--r--usr.sbin/autofs/common.c3
-rw-r--r--usr.sbin/bhyve/pci_xhci.c35
-rw-r--r--usr.sbin/bhyve/usb_emul.h4
-rw-r--r--usr.sbin/bhyve/usb_mouse.c13
-rw-r--r--usr.sbin/certctl/Makefile11
-rw-r--r--usr.sbin/certctl/certctl.896
-rw-r--r--usr.sbin/certctl/certctl.c1114
-rwxr-xr-xusr.sbin/certctl/certctl.sh366
-rw-r--r--usr.sbin/certctl/tests/Makefile5
-rw-r--r--usr.sbin/certctl/tests/certctl.subr44
-rw-r--r--usr.sbin/certctl/tests/certctl_test.sh332
-rw-r--r--usr.sbin/chroot/chroot.c38
-rw-r--r--usr.sbin/freebsd-update/freebsd-update.sh2
-rw-r--r--usr.sbin/jail/config.c2
-rwxr-xr-xusr.sbin/jail/tests/jail_basic_test.sh15
-rw-r--r--usr.sbin/lpr/lpc/lpc.c2
-rw-r--r--usr.sbin/makefs/zfs.c2
-rw-r--r--usr.sbin/makefs/zfs/vdev.c2
-rw-r--r--usr.sbin/services_mkdb/services2
-rw-r--r--usr.sbin/syslogd/syslogd.82
-rw-r--r--usr.sbin/syslogd/syslogd.c5
-rwxr-xr-xusr.sbin/unbound/setup/local-unbound-setup.sh2
-rw-r--r--usr.sbin/watchdogd/watchdogd.c112
23 files changed, 1689 insertions, 520 deletions
diff --git a/usr.sbin/autofs/common.c b/usr.sbin/autofs/common.c
index 18756752876c..2dd7c290cebc 100644
--- a/usr.sbin/autofs/common.c
+++ b/usr.sbin/autofs/common.c
@@ -149,10 +149,11 @@ create_directory(const char *path)
error = mkdir(partial, 0755);
if (error != 0 && errno != EEXIST) {
log_warn("cannot create %s", partial);
- return;
+ break;
}
}
+ free(partial);
free(tofree);
}
diff --git a/usr.sbin/bhyve/pci_xhci.c b/usr.sbin/bhyve/pci_xhci.c
index 0871bbb87fe5..ff12e40359e2 100644
--- a/usr.sbin/bhyve/pci_xhci.c
+++ b/usr.sbin/bhyve/pci_xhci.c
@@ -406,7 +406,7 @@ pci_xhci_usbcmd_write(struct pci_xhci_softc *sc, uint32_t cmd)
* XHCI 4.19.3 USB2 RxDetect->Polling,
* USB3 Polling->U0
*/
- if (dev->dev_ue->ue_usbver == 2)
+ if (dev->hci.hci_usbver == 2)
port->portsc |=
XHCI_PS_PLS_SET(UPS_PORT_LS_POLL);
else
@@ -2590,7 +2590,7 @@ pci_xhci_reset_port(struct pci_xhci_softc *sc, int portn, int warm)
port->portsc |= XHCI_PS_PED |
XHCI_PS_SPEED_SET(dev->hci.hci_speed);
- if (warm && dev->dev_ue->ue_usbver == 3) {
+ if (warm && dev->hci.hci_usbver == 3) {
port->portsc |= XHCI_PS_WRC;
}
@@ -2620,7 +2620,7 @@ pci_xhci_init_port(struct pci_xhci_softc *sc, int portn)
port->portsc = XHCI_PS_CCS | /* connected */
XHCI_PS_PP; /* port power */
- if (dev->dev_ue->ue_usbver == 2) {
+ if (dev->hci.hci_usbver == 2) {
port->portsc |= XHCI_PS_PLS_SET(UPS_PORT_LS_POLL) |
XHCI_PS_SPEED_SET(dev->hci.hci_speed);
} else {
@@ -2785,8 +2785,8 @@ pci_xhci_parse_devices(struct pci_xhci_softc *sc, nvlist_t *nvl)
cookie = NULL;
while ((name = nvlist_next(slots_nvl, &type, &cookie)) != NULL) {
- if (usb2_port == ((sc->usb2_port_start) + XHCI_MAX_DEVS/2) ||
- usb3_port == ((sc->usb3_port_start) + XHCI_MAX_DEVS/2)) {
+ if (usb2_port == ((sc->usb2_port_start) + XHCI_MAX_DEVS / 2) ||
+ usb3_port == ((sc->usb3_port_start) + XHCI_MAX_DEVS / 2)) {
WPRINTF(("pci_xhci max number of USB 2 or 3 "
"devices reached, max %d", XHCI_MAX_DEVS/2));
goto bad;
@@ -2834,12 +2834,25 @@ pci_xhci_parse_devices(struct pci_xhci_softc *sc, nvlist_t *nvl)
dev->hci.hci_intr = pci_xhci_dev_intr;
dev->hci.hci_event = pci_xhci_dev_event;
dev->hci.hci_speed = USB_SPEED_MAX;
+ dev->hci.hci_usbver = -1;
- if (ue->ue_usbver == 2) {
+ devsc = ue->ue_probe(&dev->hci, nvl);
+ if (devsc == NULL) {
+ free(dev);
+ goto bad;
+ }
+ dev->dev_sc = devsc;
+
+ if (dev->hci.hci_usbver == -1)
+ dev->hci.hci_usbver = ue->ue_usbver;
+
+ if (dev->hci.hci_usbver == 2) {
if (usb2_port == sc->usb2_port_start +
XHCI_MAX_DEVS / 2) {
WPRINTF(("pci_xhci max number of USB 2 devices "
"reached, max %d", XHCI_MAX_DEVS / 2));
+ free(dev->dev_sc);
+ free(dev);
goto bad;
}
dev->hci.hci_port = usb2_port;
@@ -2849,6 +2862,8 @@ pci_xhci_parse_devices(struct pci_xhci_softc *sc, nvlist_t *nvl)
XHCI_MAX_DEVS / 2) {
WPRINTF(("pci_xhci max number of USB 3 devices "
"reached, max %d", XHCI_MAX_DEVS / 2));
+ free(dev->dev_sc);
+ free(dev);
goto bad;
}
dev->hci.hci_port = usb3_port;
@@ -2857,13 +2872,10 @@ pci_xhci_parse_devices(struct pci_xhci_softc *sc, nvlist_t *nvl)
XHCI_DEVINST_PTR(sc, dev->hci.hci_port) = dev;
dev->hci.hci_address = 0;
- devsc = ue->ue_init(&dev->hci, nvl);
- if (devsc == NULL) {
+ if (ue->ue_init(dev->dev_sc))
goto bad;
- }
dev->dev_ue = ue;
- dev->dev_sc = devsc;
if (dev->hci.hci_speed == USB_SPEED_MAX)
dev->hci.hci_speed = ue->ue_usbspeed;
@@ -2885,6 +2897,8 @@ portsfinal:
bad:
for (i = 1; i <= XHCI_MAX_DEVS; i++) {
+ if (XHCI_DEVINST_PTR(sc, i) != NULL)
+ free(XHCI_DEVINST_PTR(sc, i)->dev_sc);
free(XHCI_DEVINST_PTR(sc, i));
}
@@ -3232,6 +3246,7 @@ pci_xhci_snapshot(struct vm_snapshot_meta *meta)
SNAPSHOT_VAR_OR_LEAVE(dev->hci.hci_address, meta, ret, done);
SNAPSHOT_VAR_OR_LEAVE(dev->hci.hci_port, meta, ret, done);
SNAPSHOT_VAR_OR_LEAVE(dev->hci.hci_speed, meta, ret, done);
+ SNAPSHOT_VAR_OR_LEAVE(dev->hci.hci_usbver, meta, ret, done);
}
SNAPSHOT_VAR_OR_LEAVE(sc->usb2_port_start, meta, ret, done);
diff --git a/usr.sbin/bhyve/usb_emul.h b/usr.sbin/bhyve/usb_emul.h
index 85dedfeacd3b..43b6b53b5205 100644
--- a/usr.sbin/bhyve/usb_emul.h
+++ b/usr.sbin/bhyve/usb_emul.h
@@ -52,7 +52,8 @@ struct usb_devemu {
int ue_usbspeed; /* usb device speed */
/* instance creation */
- void *(*ue_init)(struct usb_hci *hci, nvlist_t *nvl);
+ void *(*ue_probe)(struct usb_hci *hci, nvlist_t *nvl);
+ int (*ue_init)(void *sc);
/* handlers */
int (*ue_request)(void *sc, struct usb_data_xfer *xfer);
@@ -86,6 +87,7 @@ struct usb_hci {
int hci_address;
int hci_port;
int hci_speed;
+ int hci_usbver;
};
/*
diff --git a/usr.sbin/bhyve/usb_mouse.c b/usr.sbin/bhyve/usb_mouse.c
index a37941c0cd9d..82b1159d5f61 100644
--- a/usr.sbin/bhyve/usb_mouse.c
+++ b/usr.sbin/bhyve/usb_mouse.c
@@ -295,20 +295,28 @@ umouse_event(uint8_t button, int x, int y, void *arg)
}
static void *
-umouse_init(struct usb_hci *hci, nvlist_t *nvl __unused)
+umouse_probe(struct usb_hci *hci, nvlist_t *nvl __unused)
{
struct umouse_softc *sc;
sc = calloc(1, sizeof(struct umouse_softc));
sc->hci = hci;
+ return (sc);
+}
+
+static int
+umouse_init(void *scarg)
+{
+ struct umouse_softc *sc = (struct umouse_softc *)scarg;
+
sc->hid.protocol = 1; /* REPORT protocol */
pthread_mutex_init(&sc->mtx, NULL);
pthread_mutex_init(&sc->ev_mtx, NULL);
console_ptr_register(umouse_event, sc, 10);
- return (sc);
+ return (0);
}
#define UREQ(x,y) ((x) | ((y) << 8))
@@ -811,6 +819,7 @@ static struct usb_devemu ue_mouse = {
.ue_emu = "tablet",
.ue_usbver = 3,
.ue_usbspeed = USB_SPEED_HIGH,
+ .ue_probe = umouse_probe,
.ue_init = umouse_init,
.ue_request = umouse_request,
.ue_data = umouse_data_handler,
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 allanjude@freebsd.org
+.An -nosplit
+The original shell implementation was written by
+.An Allan Jude Aq Mt allanjude@FreeBSD.org .
+The current C implementation was written by
+.An Dag-Erling Sm\(/orgrav Aq Mt des@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 <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();
+}
diff --git a/usr.sbin/certctl/certctl.sh b/usr.sbin/certctl/certctl.sh
deleted file mode 100755
index 2bde651de126..000000000000
--- a/usr.sbin/certctl/certctl.sh
+++ /dev/null
@@ -1,366 +0,0 @@
-#!/bin/sh
-#-
-# SPDX-License-Identifier: BSD-2-Clause
-#
-# Copyright 2018 Allan Jude <allanjude@freebsd.org>
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted providing that the following conditions
-# are met:
-# 1. Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-# 2. Redistributions in binary form must reproduce the above copyright
-# notice, this list of conditions and the following disclaimer in the
-# documentation and/or other materials provided with the distribution.
-#
-# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
-# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
-# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
-# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
-# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
-# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
-# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
-# POSSIBILITY OF SUCH DAMAGE.
-#
-
-set -u
-
-############################################################ CONFIGURATION
-
-: ${DESTDIR:=}
-: ${DISTBASE:=}
-
-############################################################ GLOBALS
-
-SCRIPTNAME="${0##*/}"
-LINK=-lrs
-ERRORS=0
-NOOP=false
-UNPRIV=false
-VERBOSE=false
-
-############################################################ FUNCTIONS
-
-info()
-{
- echo "${0##*/}: $@" >&2
-}
-
-verbose()
-{
- if "${VERBOSE}" ; then
- info "$@"
- fi
-}
-
-perform()
-{
- if ! "${NOOP}" ; then
- "$@"
- fi
-}
-
-cert_files_in()
-{
- find -L "$@" -type f \( \
- -name '*.pem' -or \
- -name '*.crt' -or \
- -name '*.cer' \
- \) 2>/dev/null
-}
-
-eolcvt()
-{
- cat "$@" | tr -s '\r' '\n'
-}
-
-do_hash()
-{
- local hash
-
- if hash=$(openssl x509 -noout -subject_hash -in "$1") ; then
- echo "$hash"
- return 0
- else
- info "Error: $1"
- ERRORS=$((ERRORS + 1))
- return 1
- fi
-}
-
-get_decimal()
-{
- local checkdir hash decimal
-
- checkdir=$1
- hash=$2
- decimal=0
-
- while [ -e "$checkdir/$hash.$decimal" ] ; do
- decimal=$((decimal + 1))
- done
-
- echo ${decimal}
- return 0
-}
-
-create_trusted()
-{
- local hash certhash otherfile otherhash
- local suffix
-
- hash=$(do_hash "$1") || return
- certhash=$(openssl x509 -sha1 -in "$1" -noout -fingerprint)
- for otherfile in $(find $UNTRUSTDESTDIR -name "$hash.*") ; do
- otherhash=$(openssl x509 -sha1 -in "$otherfile" -noout -fingerprint)
- if [ "$certhash" = "$otherhash" ] ; then
- info "Skipping untrusted certificate $hash ($otherfile)"
- return 0
- fi
- done
- for otherfile in $(find $CERTDESTDIR -name "$hash.*") ; do
- otherhash=$(openssl x509 -sha1 -in "$otherfile" -noout -fingerprint)
- if [ "$certhash" = "$otherhash" ] ; then
- verbose "Skipping duplicate entry for certificate $hash"
- return 0
- fi
- done
- suffix=$(get_decimal "$CERTDESTDIR" "$hash")
- verbose "Adding $hash.$suffix to trust store"
- perform install ${INSTALLFLAGS} -m 0444 ${LINK} \
- "$(realpath "$1")" "$CERTDESTDIR/$hash.$suffix"
-}
-
-# Accepts either dot-hash form from `certctl list` or a path to a valid cert.
-resolve_certname()
-{
- local hash srcfile filename
- local suffix
-
- # If it exists as a file, we'll try that; otherwise, we'll scan
- if [ -e "$1" ] ; then
- hash=$(do_hash "$1") || return
- srcfile=$(realpath "$1")
- suffix=$(get_decimal "$UNTRUSTDESTDIR" "$hash")
- filename="$hash.$suffix"
- echo "$srcfile" "$hash.$suffix"
- elif [ -e "${CERTDESTDIR}/$1" ] ; then
- srcfile=$(realpath "${CERTDESTDIR}/$1")
- hash=$(echo "$1" | sed -Ee 's/\.([0-9])+$//')
- suffix=$(get_decimal "$UNTRUSTDESTDIR" "$hash")
- filename="$hash.$suffix"
- echo "$srcfile" "$hash.$suffix"
- fi
-}
-
-create_untrusted()
-{
- local srcfile filename
-
- set -- $(resolve_certname "$1")
- srcfile=$1
- filename=$2
-
- if [ -z "$srcfile" -o -z "$filename" ] ; then
- return
- fi
-
- verbose "Adding $filename to untrusted list"
- perform install ${INSTALLFLAGS} -m 0444 ${LINK} \
- "$srcfile" "$UNTRUSTDESTDIR/$filename"
-}
-
-do_scan()
-{
- local CFUNC CSEARCH CPATH CFILE CERT SPLITDIR
- local oldIFS="$IFS"
- CFUNC="$1"
- CSEARCH="$2"
-
- IFS=:
- set -- $CSEARCH
- IFS="$oldIFS"
- for CFILE in $(cert_files_in "$@") ; do
- verbose "Reading $CFILE"
- case $(eolcvt "$CFILE" | egrep -c '^-+BEGIN CERTIFICATE-+$') in
- 0)
- ;;
- 1)
- "$CFUNC" "$CFILE"
- ;;
- *)
- verbose "Multiple certificates found, splitting..."
- SPLITDIR=$(mktemp -d)
- eolcvt "$CFILE" | egrep '^(---|[0-9A-Za-z/+=]+$)' | \
- split -p '^-+BEGIN CERTIFICATE-+$' - "$SPLITDIR/x"
- for CERT in $(find "$SPLITDIR" -type f) ; do
- "$CFUNC" "$CERT"
- done
- rm -rf "$SPLITDIR"
- ;;
- esac
- done
-}
-
-do_list()
-{
- local CFILE subject
-
- for CFILE in $(find "$@" \( -type f -or -type l \) -name '*.[0-9]') ; do
- if [ ! -s "$CFILE" ] ; then
- info "Unable to read $CFILE"
- ERRORS=$((ERRORS + 1))
- continue
- fi
- subject=
- if ! "$VERBOSE" ; then
- subject=$(openssl x509 -noout -subject -nameopt multiline -in "$CFILE" | sed -n '/commonName/s/.*= //p')
- fi
- if [ -z "$subject" ] ; then
- subject=$(openssl x509 -noout -subject -in "$CFILE")
- fi
- printf "%s\t%s\n" "${CFILE##*/}" "$subject"
- done
-}
-
-cmd_rehash()
-{
-
- if [ -e "$CERTDESTDIR" ] ; then
- perform find "$CERTDESTDIR" \( -type f -or -type l \) -delete
- else
- perform install -d -m 0755 "$CERTDESTDIR"
- fi
- if [ -e "$UNTRUSTDESTDIR" ] ; then
- perform find "$UNTRUSTDESTDIR" \( -type f -or -type l \) -delete
- else
- perform install -d -m 0755 "$UNTRUSTDESTDIR"
- fi
-
- do_scan create_untrusted "$UNTRUSTPATH"
- do_scan create_trusted "$TRUSTPATH"
-}
-
-cmd_list()
-{
- info "Listing Trusted Certificates:"
- do_list "$CERTDESTDIR"
-}
-
-cmd_untrust()
-{
- local UTFILE
-
- shift # verb
- perform install -d -m 0755 "$UNTRUSTDESTDIR"
- for UTFILE in "$@"; do
- info "Adding $UTFILE to untrusted list"
- create_untrusted "$UTFILE"
- done
-}
-
-cmd_trust()
-{
- local UTFILE untrustedhash certhash hash
-
- shift # verb
- for UTFILE in "$@"; do
- if [ -s "$UTFILE" ] ; then
- hash=$(do_hash "$UTFILE")
- certhash=$(openssl x509 -sha1 -in "$UTFILE" -noout -fingerprint)
- for UNTRUSTEDFILE in $(find $UNTRUSTDESTDIR -name "$hash.*") ; do
- untrustedhash=$(openssl x509 -sha1 -in "$UNTRUSTEDFILE" -noout -fingerprint)
- if [ "$certhash" = "$untrustedhash" ] ; then
- info "Removing $(basename "$UNTRUSTEDFILE") from untrusted list"
- perform rm -f $UNTRUSTEDFILE
- fi
- done
- elif [ -e "$UNTRUSTDESTDIR/$UTFILE" ] ; then
- info "Removing $UTFILE from untrusted list"
- perform rm -f "$UNTRUSTDESTDIR/$UTFILE"
- else
- info "Cannot find $UTFILE"
- ERRORS=$((ERRORS + 1))
- fi
- done
-}
-
-cmd_untrusted()
-{
- info "Listing Untrusted Certificates:"
- do_list "$UNTRUSTDESTDIR"
-}
-
-usage()
-{
- exec >&2
- echo "Manage the TLS trusted certificates on the system"
- echo " $SCRIPTNAME [-v] list"
- echo " List trusted certificates"
- echo " $SCRIPTNAME [-v] untrusted"
- echo " List untrusted certificates"
- echo " $SCRIPTNAME [-cnUv] [-D <destdir>] [-d <distbase>] [-M <metalog>] rehash"
- echo " Rehash all trusted and untrusted certificates"
- echo " $SCRIPTNAME [-cnv] untrust <file>"
- echo " Add <file> to the list of untrusted certificates"
- echo " $SCRIPTNAME [-cnv] trust <file>"
- echo " Remove <file> from the list of untrusted certificates"
- exit 64
-}
-
-############################################################ MAIN
-
-while getopts cD:d:M:nUv flag; do
- case "$flag" in
- c) LINK=-c ;;
- D) DESTDIR=${OPTARG} ;;
- d) DISTBASE=${OPTARG} ;;
- M) METALOG=${OPTARG} ;;
- n) NOOP=true ;;
- U) UNPRIV=true ;;
- v) VERBOSE=true ;;
- esac
-done
-shift $((OPTIND - 1))
-
-DESTDIR=${DESTDIR%/}
-
-if ! [ -z "${CERTCTL_VERBOSE:-}" ] ; then
- VERBOSE=true
-fi
-: ${METALOG:=${DESTDIR}/METALOG}
-INSTALLFLAGS=
-if "$UNPRIV" ; then
- INSTALLFLAGS="-U -M ${METALOG} -D ${DESTDIR:-/} -o root -g wheel"
-fi
-: ${LOCALBASE:=$(sysctl -n user.localbase)}
-: ${TRUSTPATH:=${DESTDIR}${DISTBASE}/usr/share/certs/trusted:${DESTDIR}${LOCALBASE}/share/certs:${DESTDIR}${LOCALBASE}/etc/ssl/certs}
-: ${UNTRUSTPATH:=${DESTDIR}${DISTBASE}/usr/share/certs/untrusted:${DESTDIR}${LOCALBASE}/etc/ssl/untrusted:${DESTDIR}${LOCALBASE}/etc/ssl/blacklisted}
-: ${CERTDESTDIR:=${DESTDIR}${DISTBASE}/etc/ssl/certs}
-: ${UNTRUSTDESTDIR:=${DESTDIR}${DISTBASE}/etc/ssl/untrusted}
-
-[ $# -gt 0 ] || usage
-case "$1" in
-list) cmd_list ;;
-rehash) cmd_rehash ;;
-blacklist) cmd_untrust "$@" ;;
-untrust) cmd_untrust "$@" ;;
-trust) cmd_trust "$@" ;;
-unblacklist) cmd_trust "$@" ;;
-untrusted) cmd_untrusted ;;
-blacklisted) cmd_untrusted ;;
-*) usage # NOTREACHED
-esac
-
-retval=$?
-if [ $ERRORS -gt 0 ] ; then
- info "Encountered $ERRORS errors"
-fi
-exit $retval
-
-################################################################################
-# END
-################################################################################
diff --git a/usr.sbin/certctl/tests/Makefile b/usr.sbin/certctl/tests/Makefile
new file mode 100644
index 000000000000..da301c3ded03
--- /dev/null
+++ b/usr.sbin/certctl/tests/Makefile
@@ -0,0 +1,5 @@
+PACKAGE= tests
+ATF_TESTS_SH= certctl_test
+${PACKAGE}FILES+= certctl.subr
+
+.include <bsd.test.mk>
diff --git a/usr.sbin/certctl/tests/certctl.subr b/usr.sbin/certctl/tests/certctl.subr
new file mode 100644
index 000000000000..841cc1781e69
--- /dev/null
+++ b/usr.sbin/certctl/tests/certctl.subr
@@ -0,0 +1,44 @@
+#
+# Copyright (c) 2025 Dag-Erling Smørgrav <des@FreeBSD.org>
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+
+# Generate a random name
+rand_name() {
+ local length=${1:-32}
+
+ jot -r -c -s '' ${length} A Z
+}
+
+# Generate a subject for a given name
+subject() {
+ local crtname=$1
+
+ echo "/CN=${crtname}/O=FreeBSD/OU=Test/"
+}
+
+# Generate a key
+gen_key() {
+ local keyname=$1
+
+ env -i PATH="${PATH}" OPENSSL_CONF=/dev/null \
+ openssl genrsa -out ${keyname}.key
+}
+
+# Generate a certificate for a given name, key, and serial number
+gen_crt() {
+ local crtname=$1
+ local keyname=${2:-${crtname}}
+ local serial=${3:-1}
+
+ if ! [ -f "${keyname}".key ]; then
+ gen_key "${keyname}"
+ fi
+ env -i PATH="${PATH}" OPENSSL_CONF=/dev/null \
+ openssl req -x509 -new \
+ -subj="$(subject ${crtname})" \
+ -set_serial ${serial} \
+ -key ${keyname}.key \
+ -out ${crtname}.crt
+}
diff --git a/usr.sbin/certctl/tests/certctl_test.sh b/usr.sbin/certctl/tests/certctl_test.sh
new file mode 100644
index 000000000000..f60bac6ffbb3
--- /dev/null
+++ b/usr.sbin/certctl/tests/certctl_test.sh
@@ -0,0 +1,332 @@
+#
+# Copyright (c) 2025 Dag-Erling Smørgrav <des@FreeBSD.org>
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+
+. $(atf_get_srcdir)/certctl.subr
+
+# Random sets of eight non-colliding names
+set1()
+{
+ cat <<EOF
+AVOYKJHSLFHWPVQMKBHENUAHJTEGMCCB 0ca83bbe
+UYSYXKDNNJTYOQPBGIKQDHRJYZHTDPKK 0d9a6512
+LODHGFXMZYKGOKAYGWTMMYQJYHDATDDM 4e6219f5
+NBBTQHJLHKBFFFWJTHHSNKOQYMGLHLPW 5dd76abc
+BJFAQZXZHYQLIDDPCAQFPDMNXICUXBXW ad68573d
+IOKNTHVEVVIJMNMYAVILMEMQQWLVRESN b577803d
+BHGMAJJGNJPIVMHMFCUTJLGFROJICEKN c98a6338
+HCRFQMGDQJALMLUQNXMPGLXFLLJRODJW f50c6379
+EOF
+}
+
+set2()
+{
+ cat <<EOF
+GOHKZTSKIPDSYNLMGYXGLROPTATELXIU 30789c88
+YOOTYHEGHZIYFXOBLNKENPSJUDGOPJJU 7fadbc13
+ETRINNYBGKIENAVGOKVJYFSSHFZIJZRH 8ed664af
+DBFGMFFMRNLPQLQPOLXOEUVLCRXLRSWT 8f34355e
+WFOPBQPLQFHDHZOUQFEIDGSYDUOTSNDQ ac0471df
+HMNETZMGNIWRGXQCVZXVZGWSGFBRRDQC b32f1472
+SHFYBXDVAUACBFPPAIGDAQIAGYOYGMQE baca75fa
+PCBGDNVPYCDGNRQSGRSLXFHYKXLAVLHW ddeeae01
+EOF
+}
+
+set3()
+{
+ cat <<EOF
+NJWIRLPWAIICVJBKXXHFHLCPAERZATRL 000aa2e5
+RJAENDPOCZQEVCPFUWOWDXPCSMYJPVYC 021b95a3
+PQUQDSWHBNVLBTNBGONYRLGZZVEFXVLO 071e8c50
+VZEXRKJUPZSFBDWBOLUZXOGLNTEAPCZM 3af7bb9b
+ZXOWOXQTXNZMAMZIWVFDZDJEWOOAGAOH 48d5c7cc
+KQSFQYVJMFTMADIHJIWGSQISWKSHRYQO 509f5ba1
+AIECYSLWZOIEPJWWUTWSQXCNCIHHZHYI 8cb0c503
+RFHWDJZEPOFLMPGXAHVEJFHCDODAPVEV 9ae4e049
+EOF
+}
+
+# Random set of three colliding names
+collhash=f2888ce3
+coll()
+{
+ cat <<EOF
+EJFTZEOANQLOYPEHWWXBWEWEFVKHMSNA $collhash
+LEMRWZAZLKZLPPSFLNLQZVGKKBEOFYWG $collhash
+ZWUPHYWKKTVEFBJOLLPDAIKGRDFVXZID $collhash
+EOF
+}
+
+sortfile() {
+ for filename; do
+ sort "${filename}" >"${filename}"-
+ mv "${filename}"- "${filename}"
+ done
+}
+
+certctl_setup()
+{
+ export DESTDIR="$PWD"
+
+ # Create input directories
+ mkdir -p ${DESTDIR}${DISTBASE}/usr/share/certs/trusted
+ mkdir -p ${DESTDIR}${DISTBASE}/usr/share/certs/untrusted
+ mkdir -p ${DESTDIR}/usr/local/share/certs
+
+ # Create output directories
+ mkdir -p ${DESTDIR}${DISTBASE}/etc/ssl/certs
+ mkdir -p ${DESTDIR}${DISTBASE}/etc/ssl/untrusted
+
+ # Generate a random key
+ keyname="testkey"
+ gen_key ${keyname}
+
+ # Generate certificates
+ :>metalog.expect
+ :>trusted.expect
+ :>untrusted.expect
+ metalog() {
+ echo ".${DISTBASE}$@ type=file" >>metalog.expect
+ }
+ trusted() {
+ local crtname=$1
+ local filename=$2
+ printf "%s\t%s\n" "${filename}" "${crtname}" >>trusted.expect
+ metalog "/etc/ssl/certs/${filename}"
+ }
+ untrusted() {
+ local crtname=$1
+ local filename=$2
+ printf "%s\t%s\n" "${filename}" "${crtname}" >>untrusted.expect
+ metalog "/etc/ssl/untrusted/${filename}"
+ }
+ set1 | while read crtname hash ; do
+ gen_crt ${crtname} ${keyname}
+ mv ${crtname}.crt ${DESTDIR}${DISTBASE}/usr/share/certs/trusted
+ trusted "${crtname}" "${hash}.0"
+ done
+ local c=0
+ coll | while read crtname hash ; do
+ gen_crt ${crtname} ${keyname}
+ mv ${crtname}.crt ${DESTDIR}${DISTBASE}/usr/share/certs/trusted
+ trusted "${crtname}" "${hash}.${c}"
+ c=$((c+1))
+ done
+ set2 | while read crtname hash ; do
+ gen_crt ${crtname} ${keyname}
+ openssl x509 -in ${crtname}.crt
+ rm ${crtname}.crt
+ trusted "${crtname}" "${hash}.0"
+ done >usr/local/share/certs/bundle.crt
+ set3 | while read crtname hash ; do
+ gen_crt ${crtname} ${keyname}
+ mv ${crtname}.crt ${DESTDIR}${DISTBASE}/usr/share/certs/untrusted
+ untrusted "${crtname}" "${hash}.0"
+ done
+ metalog "/etc/ssl/cert.pem"
+ unset -f untrusted
+ unset -f trusted
+ unset -f metalog
+ sortfile *.expect
+}
+
+check_trusted() {
+ local crtname=$1
+ local subject="$(subject ${crtname})"
+ local c=${2:-1}
+
+ atf_check -e ignore -o match:"found: ${c}\$" \
+ openssl storeutl -noout -subject "${subject}" \
+ ${DESTDIR}${DISTBASE}/etc/ssl/certs
+ atf_check -e ignore -o not-match:"found: [1-9]" \
+ openssl storeutl -noout -subject "${subject}" \
+ ${DESTDIR}${DISTBASE}/etc/ssl/untrusted
+}
+
+check_untrusted() {
+ local crtname=$1
+ local subject="$(subject ${crtname})"
+ local c=${2:-1}
+
+ atf_check -e ignore -o not-match:"found: [1-9]" \
+ openssl storeutl -noout -subject "${subject}" \
+ ${DESTDIR}/${DISTBASE}/etc/ssl/certs
+ atf_check -e ignore -o match:"found: ${c}\$" \
+ openssl storeutl -noout -subject "${subject}" \
+ ${DESTDIR}/${DISTBASE}/etc/ssl/untrusted
+}
+
+check_in_bundle() {
+ local b=${DISTBASE}${DISTBASE+/}
+ local crtfile=$1
+ local line
+
+ line=$(tail +5 "${crtfile}" | head -1)
+ atf_check grep -q "${line}" ${DESTDIR}${DISTBASE}/etc/ssl/cert.pem
+}
+
+check_not_in_bundle() {
+ local b=${DISTBASE}${DISTBASE+/}
+ local crtfile=$1
+ local line
+
+ line=$(tail +5 "${crtfile}" | head -1)
+ atf_check -s exit:1 grep -q "${line}" etc/ssl/cert.pem
+}
+
+atf_test_case rehash
+rehash_head()
+{
+ atf_set "descr" "Test the rehash command"
+}
+rehash_body()
+{
+ certctl_setup
+ atf_check certctl rehash
+
+ # Verify non-colliding trusted certificates
+ (set1; set2) >trusted
+ while read crtname hash ; do
+ check_trusted "${crtname}"
+ done <trusted
+
+ # Verify colliding trusted certificates
+ coll >coll
+ while read crtname hash ; do
+ check_trusted "${crtname}" $(wc -l <coll)
+ done <coll
+
+ # Verify untrusted certificates
+ set3 >untrusted
+ while read crtname hash ; do
+ check_untrusted "${crtname}"
+ done <untrusted
+
+ # Verify bundle
+ for f in etc/ssl/certs/*.? ; do
+ check_in_bundle "${f}"
+ done
+ for f in etc/ssl/untrusted/*.? ; do
+ check_not_in_bundle "${f}"
+ done
+}
+
+atf_test_case list
+list_head()
+{
+ atf_set "descr" "Test the list and untrusted commands"
+}
+list_body()
+{
+ certctl_setup
+ atf_check certctl rehash
+
+ atf_check -o save:trusted.out certctl list
+ sortfile trusted.out
+ # the ordering of the colliding certificates is partly
+ # determined by fields that change every time we regenerate
+ # them, so ignore them in the diff
+ atf_check diff -u \
+ --ignore-matching-lines $collhash \
+ trusted.expect trusted.out
+
+ atf_check -o save:untrusted.out certctl untrusted
+ sortfile untrusted.out
+ atf_check diff -u \
+ untrusted.expect untrusted.out
+}
+
+atf_test_case trust
+trust_head()
+{
+ atf_set "descr" "Test the trust command"
+}
+trust_body()
+{
+ certctl_setup
+ atf_check certctl rehash
+ crtname=$(set3 | (read crtname hash ; echo ${crtname}))
+ crtfile=usr/share/certs/untrusted/${crtname}.crt
+ check_untrusted ${crtname}
+ check_not_in_bundle ${crtfile}
+ atf_check -e match:"was previously untrusted" \
+ certctl trust ${crtfile}
+ check_trusted ${crtname}
+ check_in_bundle ${crtfile}
+}
+
+atf_test_case untrust
+untrust_head()
+{
+ atf_set "descr" "Test the untrust command"
+}
+untrust_body()
+{
+ certctl_setup
+ atf_check certctl rehash
+ crtname=$(set1 | (read crtname hash ; echo ${crtname}))
+ crtfile=usr/share/certs/trusted/${crtname}.crt
+ check_trusted "${crtname}"
+ check_in_bundle ${crtfile}
+ atf_check certctl untrust "${crtfile}"
+ check_untrusted "${crtname}"
+ check_not_in_bundle ${crtfile}
+}
+
+atf_test_case metalog
+metalog_head()
+{
+ atf_set "descr" "Verify the metalog"
+}
+metalog_body()
+{
+ export DISTBASE=/base
+ certctl_setup
+
+ # certctl gets DESTDIR and DISTBASE from environment
+ rm -f metalog.orig
+ atf_check certctl -U -M metalog.orig rehash
+ sed -E 's/(type=file) .*/\1/' metalog.orig | sort >metalog.short
+ atf_check diff -u metalog.expect metalog.short
+
+ # certctl gets DESTDIR and DISTBASE from command line
+ rm -f metalog.orig
+ atf_check env -uDESTDIR -uDISTBASE \
+ certctl -D ${DESTDIR} -d ${DISTBASE} -U -M metalog.orig rehash
+ sed -E 's/(type=file) .*/\1/' metalog.orig | sort >metalog.short
+ atf_check diff -u metalog.expect metalog.short
+
+ # as above, but intentionally add trailing slashes
+ rm -f metalog.orig
+ atf_check env -uDESTDIR -uDISTBASE \
+ certctl -D ${DESTDIR}// -d ${DISTBASE}/ -U -M metalog.orig rehash
+ sed -E 's/(type=file) .*/\1/' metalog.orig | sort >metalog.short
+ atf_check diff -u metalog.expect metalog.short
+}
+
+atf_test_case misc
+misc_head()
+{
+ atf_set "descr" "Test miscellaneous edge cases"
+}
+misc_body()
+{
+ # certctl rejects DISTBASE that does not begin with a slash
+ atf_check -s exit:1 -e match:"begin with a slash" \
+ certctl -d base -n rehash
+ atf_check -s exit:1 -e match:"begin with a slash" \
+ env DISTBASE=base certctl -n rehash
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case rehash
+ atf_add_test_case list
+ atf_add_test_case trust
+ atf_add_test_case untrust
+ atf_add_test_case metalog
+ atf_add_test_case misc
+}
diff --git a/usr.sbin/chroot/chroot.c b/usr.sbin/chroot/chroot.c
index d9fb29474d87..e1af0a4131d3 100644
--- a/usr.sbin/chroot/chroot.c
+++ b/usr.sbin/chroot/chroot.c
@@ -103,7 +103,9 @@ main(int argc, char *argv[])
gid = 0;
uid = 0;
+ gids = 0;
user = group = grouplist = NULL;
+ gidlist = NULL;
nonprivileged = false;
while ((ch = getopt(argc, argv, "G:g:u:n")) != -1) {
switch(ch) {
@@ -119,6 +121,11 @@ main(int argc, char *argv[])
break;
case 'G':
grouplist = optarg;
+
+ /*
+ * XXX Why not allow us to drop all of our supplementary
+ * groups?
+ */
if (*grouplist == '\0')
usage();
break;
@@ -139,23 +146,20 @@ main(int argc, char *argv[])
if (group != NULL)
gid = resolve_group(group);
- ngroups_max = sysconf(_SC_NGROUPS_MAX) + 1;
- if ((gidlist = malloc(sizeof(gid_t) * ngroups_max)) == NULL)
- err(1, "malloc");
- /* Populate the egid slot in our groups to avoid accidents. */
- if (gid == 0)
- gidlist[0] = getegid();
- else
- gidlist[0] = gid;
- for (gids = 1;
- (p = strsep(&grouplist, ",")) != NULL && gids < ngroups_max; ) {
- if (*p == '\0')
- continue;
-
- gidlist[gids++] = resolve_group(p);
+ if (grouplist != NULL) {
+ ngroups_max = sysconf(_SC_NGROUPS_MAX);
+ if ((gidlist = malloc(sizeof(gid_t) * ngroups_max)) == NULL)
+ err(1, "malloc");
+ for (gids = 0; (p = strsep(&grouplist, ",")) != NULL &&
+ gids < ngroups_max; ) {
+ if (*p == '\0')
+ continue;
+
+ gidlist[gids++] = resolve_group(p);
+ }
+ if (p != NULL && gids == ngroups_max)
+ errx(1, "too many supplementary groups provided");
}
- if (p != NULL && gids == ngroups_max)
- errx(1, "too many supplementary groups provided");
if (user != NULL)
uid = resolve_user(user);
@@ -175,7 +179,7 @@ main(int argc, char *argv[])
err(1, "%s", argv[0]);
}
- if (gids && setgroups(gids, gidlist) == -1)
+ if (gidlist != NULL && setgroups(gids, gidlist) == -1)
err(1, "setgroups");
if (group && setgid(gid) == -1)
err(1, "setgid");
diff --git a/usr.sbin/freebsd-update/freebsd-update.sh b/usr.sbin/freebsd-update/freebsd-update.sh
index c388e76644dc..81040431ea79 100644
--- a/usr.sbin/freebsd-update/freebsd-update.sh
+++ b/usr.sbin/freebsd-update/freebsd-update.sh
@@ -3000,7 +3000,7 @@ install_from_index () {
if [ -z "${LINK}" ]; then
# Create a file, without setting flags.
gunzip < files/${HASH}.gz > ${HASH}
- install -S -o ${OWNER} -g ${GROUP} \
+ install -o ${OWNER} -g ${GROUP} \
-m ${PERM} ${HASH} ${BASEDIR}/${FPATH}
rm ${HASH}
else
diff --git a/usr.sbin/jail/config.c b/usr.sbin/jail/config.c
index 70de82e662e7..1bad04ccde68 100644
--- a/usr.sbin/jail/config.c
+++ b/usr.sbin/jail/config.c
@@ -189,7 +189,7 @@ load_config(const char *cfname)
* jail is created or found.
*/
if (j->intparams[KP_NAME] == NULL)
- add_param(j, j->intparams[KP_JID], KP_NAME, NULL);
+ add_param(j, NULL, KP_NAME, j->name);
/* Resolve any variable substitutions. */
pgen = 0;
diff --git a/usr.sbin/jail/tests/jail_basic_test.sh b/usr.sbin/jail/tests/jail_basic_test.sh
index 509900e8569c..6802da7b049a 100755
--- a/usr.sbin/jail/tests/jail_basic_test.sh
+++ b/usr.sbin/jail/tests/jail_basic_test.sh
@@ -198,7 +198,7 @@ clean_jails()
fi
while read jail; do
- if jls -e -j "$jail"; then
+ if jls -c -j "$jail"; then
jail -r "$jail"
fi
done < jails.lst
@@ -211,10 +211,23 @@ jid_name_set_body()
echo "basejail" >> jails.lst
echo "$jid { name = basejail; persist; }" > jail.conf
atf_check -o match:"$jid: created" jail -f jail.conf -c "$jid"
+ # Confirm that we didn't override the explicitly-set name with the jid
+ # as the name.
+ atf_check -o match:"basejail" jls -j "$jid" name
+ atf_check -o match:"$jid: removed" jail -f jail.conf -r "$jid"
+
+ echo "$jid { host.hostname = \"\${name}\"; persist; }" > jail.conf
+ atf_check -o match:"$jid: created" jail -f jail.conf -c "$jid"
+ # Confirm that ${name} expanded and expanded correctly to the
+ # jid-implied name.
+ atf_check -o match:"$jid" jls -j "$jid" host.hostname
atf_check -o match:"$jid: removed" jail -f jail.conf -r "$jid"
echo "basejail { jid = $jid; persist; }" > jail.conf
atf_check -o match:"basejail: created" jail -f jail.conf -c basejail
+ # Confirm that our jid assigment in the definition worked out and we
+ # did in-fact create the jail there.
+ atf_check -o match:"$jid" jls -j "basejail" jid
atf_check -o match:"basejail: removed" jail -f jail.conf -r basejail
}
diff --git a/usr.sbin/lpr/lpc/lpc.c b/usr.sbin/lpr/lpc/lpc.c
index a3da852de46e..b4db5bb2e29f 100644
--- a/usr.sbin/lpr/lpc/lpc.c
+++ b/usr.sbin/lpr/lpc/lpc.c
@@ -358,6 +358,8 @@ ingroup(const char *grname)
err(1, "getgroups");
}
gid = gptr->gr_gid;
+ if (gid == getegid())
+ return(1);
for (i = 0; i < ngroups; i++)
if (gid == groups[i])
return(1);
diff --git a/usr.sbin/makefs/zfs.c b/usr.sbin/makefs/zfs.c
index 8d50c450541b..e33a182e5c8f 100644
--- a/usr.sbin/makefs/zfs.c
+++ b/usr.sbin/makefs/zfs.c
@@ -596,7 +596,7 @@ pool_labels_write(zfs_opt_t *zfs)
* checksum is calculated in vdev_label_write().
*/
for (size_t uoff = 0; uoff < sizeof(label->vl_uberblock);
- uoff += (1 << zfs->ashift)) {
+ uoff += ASHIFT_UBERBLOCK_SIZE(zfs->ashift)) {
ub = (uberblock_t *)(&label->vl_uberblock[0] + uoff);
ub->ub_magic = UBERBLOCK_MAGIC;
ub->ub_version = SPA_VERSION;
diff --git a/usr.sbin/makefs/zfs/vdev.c b/usr.sbin/makefs/zfs/vdev.c
index afcce402cb13..a2423e180ca3 100644
--- a/usr.sbin/makefs/zfs/vdev.c
+++ b/usr.sbin/makefs/zfs/vdev.c
@@ -200,7 +200,7 @@ vdev_label_write(zfs_opt_t *zfs, int ind, const vdev_label_t *labelp)
* per sector; for example, with an ashift of 12 we end up with
* 128KB/4KB=32 copies of the uberblock in the ring.
*/
- blksz = 1 << zfs->ashift;
+ blksz = ASHIFT_UBERBLOCK_SIZE(zfs->ashift);
assert(sizeof(label->vl_uberblock) % blksz == 0);
for (size_t roff = 0; roff < sizeof(label->vl_uberblock);
roff += blksz) {
diff --git a/usr.sbin/services_mkdb/services b/usr.sbin/services_mkdb/services
index 4a5b6863d92d..c5f950831767 100644
--- a/usr.sbin/services_mkdb/services
+++ b/usr.sbin/services_mkdb/services
@@ -893,7 +893,7 @@ biff 512/udp comsat #used by mail system to notify users
# processes on the same machine
login 513/tcp #remote login a la telnet;
# automatic authentication performed
-# based on priviledged port numbers
+# based on privileged port numbers
# and distributed data bases which
# identify "authentication domains"
who 513/udp whod #maintains data bases showing who's
diff --git a/usr.sbin/syslogd/syslogd.8 b/usr.sbin/syslogd/syslogd.8
index fa61e78eaf3e..d39d9fdc8f5a 100644
--- a/usr.sbin/syslogd/syslogd.8
+++ b/usr.sbin/syslogd/syslogd.8
@@ -403,7 +403,7 @@ The message can contain a priority code,
which should be a preceding
decimal number in angle braces,
for example,
-.Sq Aq 5 .
+.Sq <5> .
This priority code should map into the priorities defined in the
include file
.In sys/syslog.h .
diff --git a/usr.sbin/syslogd/syslogd.c b/usr.sbin/syslogd/syslogd.c
index fe7427130b78..81bbbbe66be8 100644
--- a/usr.sbin/syslogd/syslogd.c
+++ b/usr.sbin/syslogd/syslogd.c
@@ -1830,15 +1830,14 @@ fprintlog_write(struct filed *f, struct iovlist *il, int flags)
case EHOSTUNREACH:
case EHOSTDOWN:
case EADDRNOTAVAIL:
+ case EAGAIN:
+ case ECONNREFUSED:
break;
/* case EBADF: */
/* case EACCES: */
/* case ENOTSOCK: */
/* case EFAULT: */
/* case EMSGSIZE: */
- /* case EAGAIN: */
- /* case ENOBUFS: */
- /* case ECONNREFUSED: */
default:
dprintf("removing entry: errno=%d\n", e);
f->f_type = F_UNUSED;
diff --git a/usr.sbin/unbound/setup/local-unbound-setup.sh b/usr.sbin/unbound/setup/local-unbound-setup.sh
index d52534b46fa3..d57d74952fc7 100755
--- a/usr.sbin/unbound/setup/local-unbound-setup.sh
+++ b/usr.sbin/unbound/setup/local-unbound-setup.sh
@@ -259,7 +259,7 @@ gen_unbound_conf() {
echo " pidfile: ${pidfile}"
echo " auto-trust-anchor-file: ${anchor}"
if [ "${use_tls}" = "yes" ] ; then
- echo " tls-system-cert: yes"
+ echo " tls-cert-bundle: /etc/ssl/cert.pem"
fi
echo ""
if [ -f "${forward_conf}" ] ; then
diff --git a/usr.sbin/watchdogd/watchdogd.c b/usr.sbin/watchdogd/watchdogd.c
index 88b467486da1..27123f2143d0 100644
--- a/usr.sbin/watchdogd/watchdogd.c
+++ b/usr.sbin/watchdogd/watchdogd.c
@@ -63,25 +63,25 @@
static long fetchtimeout(int opt,
const char *longopt, const char *myoptarg, int zero_ok);
static void parseargs(int, char *[]);
-static int seconds_to_pow2ns(int);
static void sighandler(int);
static void watchdog_loop(void);
static int watchdog_init(void);
static int watchdog_onoff(int onoff);
-static int watchdog_patpat(u_int timeout);
+static int watchdog_patpat(sbintime_t);
static void usage(void);
-static int tstotv(struct timeval *tv, struct timespec *ts);
static int tvtohz(struct timeval *tv);
static int debugging = 0;
static int end_program = 0;
static const char *pidfile = _PATH_VARRUN "watchdogd.pid";
-static u_int timeout = WD_TO_128SEC;
+static sbintime_t timeout = 128 * SBT_1S;
static u_int exit_timeout = WD_TO_NEVER;
static u_int pretimeout = 0;
static u_int timeout_sec;
static u_int nap = 10;
+#ifdef notyet
static int passive = 0;
+#endif
static int is_daemon = 0;
static int is_dry_run = 0; /* do not arm the watchdog, only
report on timing of the watch
@@ -174,38 +174,23 @@ main(int argc, char *argv[])
pidfile_remove(pfh);
return (EX_OK);
} else {
- if (passive)
- timeout |= WD_PASSIVE;
- else
- timeout |= WD_ACTIVE;
if (watchdog_patpat(timeout) < 0)
err(EX_OSERR, "patting the dog");
return (EX_OK);
}
}
-static void
-pow2ns_to_ts(int pow2ns, struct timespec *ts)
-{
- uint64_t ns;
-
- ns = 1ULL << pow2ns;
- ts->tv_sec = ns / 1000000000ULL;
- ts->tv_nsec = ns % 1000000000ULL;
-}
-
/*
* Convert a timeout in seconds to N where 2^N nanoseconds is close to
* "seconds".
*
* The kernel expects the timeouts for watchdogs in "2^N nanosecond format".
*/
-static u_int
-parse_timeout_to_pow2ns(char opt, const char *longopt, const char *myoptarg)
+static sbintime_t
+parse_timeout_to_sbt(char opt, const char *longopt, const char *myoptarg)
{
- double a;
- u_int rv;
- struct timespec ts;
+ long a;
+ sbintime_t rv;
struct timeval tv;
int ticks;
char shortopt[] = "- ";
@@ -216,19 +201,17 @@ parse_timeout_to_pow2ns(char opt, const char *longopt, const char *myoptarg)
a = fetchtimeout(opt, longopt, myoptarg, 1);
if (a == 0)
- rv = WD_TO_NEVER;
+ rv = 0;
else
- rv = seconds_to_pow2ns(a);
- pow2ns_to_ts(rv, &ts);
- tstotv(&tv, &ts);
+ rv = a * SBT_1S;
+ tv = sbttotv(rv);
ticks = tvtohz(&tv);
if (debugging) {
printf("Timeout for %s%s "
- "is 2^%d nanoseconds "
- "(in: %s sec -> out: %jd sec %ld ns -> %d ticks)\n",
+ "is "
+ "(in: %s sec -> out: %jd sec %ld us -> %d ticks)\n",
longopt ? "-" : "", longopt ? longopt : shortopt,
- rv,
- myoptarg, (intmax_t)ts.tv_sec, ts.tv_nsec, ticks);
+ myoptarg, (intmax_t)tv.tv_sec, tv.tv_usec, ticks);
}
if (ticks <= 0) {
errx(1, "Timeout for %s%s is too small, please choose a higher timeout.", longopt ? "-" : "", longopt ? longopt : shortopt);
@@ -364,7 +347,7 @@ watchdog_loop(void)
}
if (failed == 0)
- watchdog_patpat(timeout|WD_ACTIVE);
+ watchdog_patpat(timeout);
waited = watchdog_check_dogfunction_time(&ts_start, &ts_end);
if (nap - waited > 0)
@@ -387,13 +370,22 @@ try_end:
* to keep the watchdog from firing.
*/
static int
-watchdog_patpat(u_int t)
+watchdog_patpat(sbintime_t sbt)
{
if (is_dry_run)
return 0;
- return ioctl(fd, WDIOCPATPAT, &t);
+ return ioctl(fd, WDIOC_SETTIMEOUT, &sbt);
+}
+
+static int
+watchdog_control(u_int control)
+{
+ if (is_dry_run)
+ return (0);
+
+ return ioctl(fd, WDIOC_CONTROL, &control);
}
/*
@@ -420,7 +412,7 @@ watchdog_onoff(int onoff)
warn("setting WDIOC_SETSOFT %d", softtimeout_set);
return (error);
}
- error = watchdog_patpat((timeout|WD_ACTIVE));
+ error = watchdog_patpat(timeout);
if (error) {
warn("watchdog_patpat failed");
goto failsafe;
@@ -452,12 +444,12 @@ watchdog_onoff(int onoff)
}
}
/* pat one more time for good measure */
- return watchdog_patpat((timeout|WD_ACTIVE));
+ return watchdog_patpat(timeout);
} else {
- return watchdog_patpat(exit_timeout);
+ return watchdog_control(WD_CTRL_DISABLE);
}
failsafe:
- watchdog_patpat(exit_timeout);
+ watchdog_control(WD_CTRL_DISABLE);
return (error);
}
@@ -567,15 +559,6 @@ timeout_act_str2int(const char *lopt, const char *acts)
return rv;
}
-int
-tstotv(struct timeval *tv, struct timespec *ts)
-{
-
- tv->tv_sec = ts->tv_sec;
- tv->tv_usec = ts->tv_nsec / 1000;
- return 0;
-}
-
/*
* Convert a timeval to a number of ticks.
* Mostly copied from the kernel.
@@ -647,30 +630,6 @@ tvtohz(struct timeval *tv)
return ((int)ticks);
}
-static int
-seconds_to_pow2ns(int seconds)
-{
- uint64_t power;
- uint64_t ns;
- uint64_t shifted;
-
- if (seconds <= 0)
- errx(1, "seconds %d < 0", seconds);
- ns = ((uint64_t)seconds) * 1000000000ULL;
- power = flsll(ns);
- shifted = 1ULL << power;
- if (shifted <= ns) {
- power++;
- }
- if (debugging) {
- printf("shifted %lld\n", (long long)shifted);
- printf("seconds_to_pow2ns: seconds: %d, ns %lld, power %d\n",
- seconds, (long long)ns, (int)power);
- }
- return (power);
-}
-
-
/*
* Handle the few command line arguments supported.
*/
@@ -683,8 +642,7 @@ parseargs(int argc, char *argv[])
const char *lopt;
/* Get the default value of timeout_sec from the default timeout. */
- pow2ns_to_ts(timeout, &ts);
- timeout_sec = ts.tv_sec;
+ timeout_sec = sbintime_getsec(timeout);
/*
* if we end with a 'd' aka 'watchdogd' then we are the daemon program,
@@ -727,10 +685,10 @@ parseargs(int argc, char *argv[])
break;
case 't':
timeout_sec = atoi(optarg);
- timeout = parse_timeout_to_pow2ns(c, NULL, optarg);
+ timeout = parse_timeout_to_sbt(c, NULL, optarg);
if (debugging)
- printf("Timeout is 2^%d nanoseconds\n",
- timeout);
+ printf("Timeout is %d\n",
+ (int)(timeout / SBT_1S));
break;
case 'T':
carp_thresh_seconds =
@@ -740,7 +698,7 @@ parseargs(int argc, char *argv[])
do_timedog = 1;
break;
case 'x':
- exit_timeout = parse_timeout_to_pow2ns(c, NULL, optarg);
+ exit_timeout = parse_timeout_to_sbt(c, NULL, optarg);
if (exit_timeout != 0)
exit_timeout |= WD_ACTIVE;
break;