diff --git a/Makefile.inc1 b/Makefile.inc1 --- a/Makefile.inc1 +++ b/Makefile.inc1 @@ -1021,7 +1021,8 @@ .endif .if make(distributeworld) -CERTCTLDESTDIR= ${DESTDIR}/${DISTDIR}/base +CERTCTLDESTDIR= ${DESTDIR}/${DISTDIR} +CERTCTLFLAGS+= -d /base .else CERTCTLDESTDIR= ${DESTDIR} .endif diff --git a/usr.sbin/certctl/certctl.8 b/usr.sbin/certctl/certctl.8 --- 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 15, 2025 .Dt CERTCTL 8 .Os .Sh NAME @@ -63,6 +63,8 @@ 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 @@ .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 @@ .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 --- a/usr.sbin/certctl/certctl.c +++ b/usr.sbin/certctl/certctl.c @@ -63,6 +63,7 @@ static const char *localbase; static const char *destdir; +static const char *distbase; static const char *metalog; static const char *uname = "root"; @@ -99,6 +100,28 @@ 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. */ @@ -124,14 +147,14 @@ } /* - * 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 +178,9 @@ /* * 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 +294,7 @@ 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,7 +319,17 @@ 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"); @@ -488,9 +524,10 @@ 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 +598,7 @@ } 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); @@ -925,6 +962,14 @@ 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) @@ -966,6 +1011,7 @@ 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 +1033,11 @@ 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 \n" - " certctl [-nv] [-D destdir] trust \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 \n" + " certctl [-nv] [-D destdir] [-d distbase] trust \n"); exit(1); } @@ -1001,7 +1047,7 @@ 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 +1058,9 @@ 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 --- a/usr.sbin/certctl/tests/certctl_test.sh +++ b/usr.sbin/certctl/tests/certctl_test.sh @@ -60,41 +60,77 @@ 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 + 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 @@ 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 @@ 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 @@ atf_check certctl rehash # Verify non-colliding trusted certificates - (set1 ; set2) > trusted + (set1; set2) >trusted while read crtname hash ; do check_trusted "${crtname}" done 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 }