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 14, 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 should 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))) == 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) @@ -294,7 +320,6 @@ name = X509_get_subject_name(x509); cert->hash = X509_NAME_hash_ex(name, NULL, NULL, NULL); cert->name = X509_NAME_oneline(name, NULL, 0); - cert->path = xstrdup(unexpand_path(path)); if (RB_INSERT(cert_tree, tree, cert) != NULL) errx(1, "unexpected duplicate"); info("%08lx: %s", cert->hash, strrchr(cert->name, '=') + 1); @@ -488,9 +513,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 +587,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 +951,12 @@ if (destdir == NULL && (destdir = getenv("DESTDIR")) == NULL) destdir = ""; + destdir = normalize_path(destdir); + + if (distbase == NULL && + (distbase = getenv("DISTBASE")) == NULL) + distbase = ""; + distbase = normalize_path(distbase); if (unprivileged && metalog == NULL && (metalog = getenv("METALOG")) == NULL) @@ -966,6 +998,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 +1020,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 +1034,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 +1045,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 @@ -65,36 +65,50 @@ 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 + metalog() { + echo ".${DISTBASE}$@ type=file" >>metalog.expect + } 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 + metalog "/etc/ssl/certs/${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 + metalog "/etc/ssl/certs/${hash}.${c}" + c=$((c+1)) done set2 | while read crtname hash ; do gen_crt ${crtname} ${keyname} openssl x509 -in ${crtname}.crt rm ${crtname}.crt + metalog "/etc/ssl/certs/${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 + metalog "/etc/ssl/untrusted/${hash}.0" done + metalog "/etc/ssl/cert.pem" + unfunction metalog + sort metalog.expect >metalog.expect.sorted + mv metalog.expect.sorted metalog.expect } check_trusted() { @@ -102,12 +116,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 +129,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 +166,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_init_test_cases() { atf_add_test_case rehash atf_add_test_case trust atf_add_test_case untrust + atf_add_test_case metalog }