aboutsummaryrefslogtreecommitdiff
path: root/usr.sbin/certctl
diff options
context:
space:
mode:
Diffstat (limited to 'usr.sbin/certctl')
-rw-r--r--usr.sbin/certctl/certctl.820
-rw-r--r--usr.sbin/certctl/certctl.c110
-rw-r--r--usr.sbin/certctl/tests/certctl_test.sh155
3 files changed, 239 insertions, 46 deletions
diff --git a/usr.sbin/certctl/certctl.8 b/usr.sbin/certctl/certctl.8
index 97bdc840c359..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 August 11, 2025
+.Dd August 18, 2025
.Dt CERTCTL 8
.Os
.Sh NAME
@@ -63,6 +63,8 @@ This option is only valid in conjunction with the
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.
@@ -117,7 +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
@@ -125,22 +133,22 @@ 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}/usr/share/certs/trusted
+.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}/usr/share/certs/untrusted
+.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}/etc/ssl/certs
+.Pa ${DESTDIR}${DISTBASE}/etc/ssl/certs
.It Ev UNTRUSTDESTDIR
Destination directory for symbolic links to untrusted certificates.
Default:
-.Pa ${DESTDIR}/etc/ssl/untrusted
+.Pa ${DESTDIR}${DISTBASE}/etc/ssl/untrusted
.It Ev BUNDLE
File name of bundle to produce.
.El
diff --git a/usr.sbin/certctl/certctl.c b/usr.sbin/certctl/certctl.c
index 365870167aeb..a53ed7b2b4b2 100644
--- a/usr.sbin/certctl/certctl.c
+++ b/usr.sbin/certctl/certctl.c
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
+#include <sys/types.h>
#include <sys/sysctl.h>
#include <sys/stat.h>
#include <sys/tree.h>
@@ -63,6 +64,7 @@ static bool verbose;
static const char *localbase;
static const char *destdir;
+static const char *distbase;
static const char *metalog;
static const char *uname = "root";
@@ -100,6 +102,50 @@ static char *bundle_dest;
static FILE *mlf;
/*
+ * Create a directory and its parents as needed.
+ */
+static void
+mkdirp(const char *dir)
+{
+ struct stat sb;
+ const char *sep;
+ char *parent;
+
+ if (stat(dir, &sb) == 0)
+ return;
+ if ((sep = strrchr(dir, '/')) != NULL) {
+ parent = xasprintf("%.*s", (int)(sep - dir), dir);
+ mkdirp(parent);
+ free(parent);
+ }
+ info("creating %s", dir);
+ if (mkdir(dir, 0755) != 0)
+ err(1, "mkdir %s", dir);
+}
+
+/*
+ * 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 **
@@ -124,14 +170,14 @@ split_paths(const char *str)
}
/*
- * Expand %L into LOCALBASE and prefix DESTDIR.
+ * 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", destdir, template));
+ return (xasprintf("%s%s%s", destdir, distbase, template));
}
/*
@@ -155,6 +201,9 @@ expand_paths(const char *const *templates)
/*
* 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)
@@ -268,7 +317,7 @@ read_cert(const char *path, struct cert_tree *tree, struct cert_tree *exclude)
X509_NAME *name;
struct cert *cert;
unsigned long hash;
- int ni, no;
+ int len, ni, no;
if ((f = fopen(path, "r")) == NULL) {
warn("%s", path);
@@ -293,11 +342,21 @@ read_cert(const char *path, struct cert_tree *tree, struct cert_tree *exclude)
cert->x509 = x509;
name = X509_get_subject_name(x509);
cert->hash = X509_NAME_hash_ex(name, NULL, NULL, NULL);
- cert->name = X509_NAME_oneline(name, NULL, 0);
+ 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, strrchr(cert->name, '=') + 1);
+ info("%08lx: %s", cert->hash, cert->name);
no++;
}
/*
@@ -321,7 +380,7 @@ 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 };
+ char *paths[] = { __DECONST(char *, path), NULL };
FTS *fts;
FTSENT *ent;
int fts_options = FTS_LOGICAL | FTS_NOCHDIR;
@@ -488,9 +547,10 @@ write_certs(const char *dir, struct cert_tree *tree)
free(tmppath);
tmppath = NULL;
}
+ fflush(f);
/* emit metalog */
if (mlf != NULL) {
- fprintf(mlf, "%s/%s type=file "
+ fprintf(mlf, ".%s/%s type=file "
"uname=%s gname=%s mode=%#o size=%ld\n",
unexpand_path(dir), path,
uname, gname, mode, ftell(f));
@@ -561,7 +621,7 @@ write_bundle(const char *dir, const char *file, struct cert_tree *tree)
}
if (ret == 0 && mlf != NULL) {
fprintf(mlf,
- "%s/%s type=file uname=%s gname=%s mode=%#o size=%ld\n",
+ ".%s/%s type=file uname=%s gname=%s mode=%#o size=%ld\n",
unexpand_path(dir), file, uname, gname, mode, ftell(f));
}
fclose(f);
@@ -648,7 +708,7 @@ save_trusted(void)
{
int ret;
- /* save untrusted certs */
+ mkdirp(trusted_dest);
ret = write_certs(trusted_dest, &trusted);
return (ret);
}
@@ -663,6 +723,7 @@ save_untrusted(void)
{
int ret;
+ mkdirp(untrusted_dest);
ret = write_certs(untrusted_dest, &untrusted);
return (ret);
}
@@ -684,6 +745,7 @@ save_bundle(void)
} else {
dir = xasprintf("%.*s", (int)(sep - bundle_dest), bundle_dest);
file = sep + 1;
+ mkdirp(dir);
}
ret = write_bundle(dir, file, &trusted);
free(dir);
@@ -925,6 +987,14 @@ set_defaults(void)
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)
@@ -950,22 +1020,23 @@ set_defaults(void)
if ((value = getenv("TRUSTDESTDIR")) != NULL ||
(value = getenv("CERTDESTDIR")) != NULL)
- trusted_dest = xstrdup(value);
+ trusted_dest = normalize_path(value);
else
trusted_dest = expand_path(TRUSTED_PATH);
if ((value = getenv("UNTRUSTDESTDIR")) != NULL)
- untrusted_dest = xstrdup(value);
+ untrusted_dest = normalize_path(value);
else
untrusted_dest = expand_path(UNTRUSTED_PATH);
if ((value = getenv("BUNDLE")) != NULL)
- bundle_dest = xstrdup(value);
+ bundle_dest = normalize_path(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");
}
@@ -987,11 +1058,11 @@ static struct {
static void
usage(void)
{
- fprintf(stderr, "usage: certctl [-lv] [-D destdir] list\n"
- " certctl [-lv] [-D destdir] untrusted\n"
- " certctl [-BnUv] [-D destdir] [-M metalog] rehash\n"
- " certctl [-nv] [-D destdir] untrust <file>\n"
- " certctl [-nv] [-D destdir] trust <file>\n");
+ 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);
}
@@ -1001,7 +1072,7 @@ main(int argc, char *argv[])
const char *command;
int opt;
- while ((opt = getopt(argc, argv, "BcD:g:lL:M:no:Uv")) != -1)
+ while ((opt = getopt(argc, argv, "BcD:d:g:lL:M:no:Uv")) != -1)
switch (opt) {
case 'B':
nobundle = true;
@@ -1012,6 +1083,9 @@ main(int argc, char *argv[])
case 'D':
destdir = optarg;
break;
+ case 'd':
+ distbase = optarg;
+ break;
case 'g':
gname = optarg;
break;
diff --git a/usr.sbin/certctl/tests/certctl_test.sh b/usr.sbin/certctl/tests/certctl_test.sh
index 4e236d5bfae8..74749db0b3f5 100644
--- a/usr.sbin/certctl/tests/certctl_test.sh
+++ b/usr.sbin/certctl/tests/certctl_test.sh
@@ -60,41 +60,77 @@ ZWUPHYWKKTVEFBJOLLPDAIKGRDFVXZID $collhash
EOF
}
+sortfile() {
+ for filename; do
+ sort "${filename}" >"${filename}"-
+ mv "${filename}"- "${filename}"
+ done
+}
+
certctl_setup()
{
export DESTDIR="$PWD"
# Create input directories
- mkdir -p usr/share/certs/trusted
- mkdir -p usr/share/certs/untrusted
- mkdir -p usr/local/share/certs
+ 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 etc/ssl/certs
- mkdir -p etc/ssl/untrusted
+ # Do not create output directories; certctl will take care of it
+ #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 usr/share/certs/trusted
+ 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 usr/share/certs/trusted
+ 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 usr/share/certs/untrusted
+ 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() {
@@ -102,12 +138,12 @@ check_trusted() {
local subject="$(subject ${crtname})"
local c=${2:-1}
- atf_check -o match:"found: ${c}\$" \
+ atf_check -e ignore -o match:"found: ${c}\$" \
openssl storeutl -noout -subject "${subject}" \
- etc/ssl/certs
- atf_check -o match:"found: 0\$" \
+ ${DESTDIR}${DISTBASE}/etc/ssl/certs
+ atf_check -e ignore -o not-match:"found: [1-9]" \
openssl storeutl -noout -subject "${subject}" \
- etc/ssl/untrusted
+ ${DESTDIR}${DISTBASE}/etc/ssl/untrusted
}
check_untrusted() {
@@ -115,23 +151,25 @@ check_untrusted() {
local subject="$(subject ${crtname})"
local c=${2:-1}
- atf_check -o match:"found: 0\$" \
+ atf_check -e ignore -o not-match:"found: [1-9]" \
openssl storeutl -noout -subject "${subject}" \
- etc/ssl/certs
- atf_check -o match:"found: ${c}\$" \
+ ${DESTDIR}/${DISTBASE}/etc/ssl/certs
+ atf_check -e ignore -o match:"found: ${c}\$" \
openssl storeutl -noout -subject "${subject}" \
- etc/ssl/untrusted
+ ${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}" etc/ssl/cert.pem
+ atf_check grep -q "${line}" ${DESTDIR}${DISTBASE}/etc/ssl/cert.pem
}
check_not_in_bundle() {
+ local b=${DISTBASE}${DISTBASE+/}
local crtfile=$1
local line
@@ -150,7 +188,7 @@ rehash_body()
atf_check certctl rehash
# Verify non-colliding trusted certificates
- (set1 ; set2) > trusted
+ (set1; set2) >trusted
while read crtname hash ; do
check_trusted "${crtname}"
done <trusted
@@ -167,7 +205,7 @@ rehash_body()
check_untrusted "${crtname}"
done <untrusted
- # Verify bundle; storeutl is no help here
+ # Verify bundle
for f in etc/ssl/certs/*.? ; do
check_in_bundle "${f}"
done
@@ -176,6 +214,31 @@ rehash_body()
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()
{
@@ -185,7 +248,7 @@ trust_body()
{
certctl_setup
atf_check certctl rehash
- crtname=NJWIRLPWAIICVJBKXXHFHLCPAERZATRL
+ crtname=$(set3 | (read crtname hash ; echo ${crtname}))
crtfile=usr/share/certs/untrusted/${crtname}.crt
check_untrusted ${crtname}
check_not_in_bundle ${crtfile}
@@ -204,7 +267,7 @@ untrust_body()
{
certctl_setup
atf_check certctl rehash
- crtname=AVOYKJHSLFHWPVQMKBHENUAHJTEGMCCB
+ crtname=$(set1 | (read crtname hash ; echo ${crtname}))
crtfile=usr/share/certs/trusted/${crtname}.crt
check_trusted "${crtname}"
check_in_bundle ${crtfile}
@@ -213,9 +276,57 @@ untrust_body()
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
}