certctl: Reimplement in C

Notable changes include:

* We no longer forget manually untrusted certificates when rehashing.

* Rehash will now scan the existing directory and progressively replace
  its contents with those of the new trust store.  The trust store as a
  whole is not replaced atomically, but each file within it is.

* We no longer attempt to link to the original files, but we don't copy
  them either.  Instead, we write each certificate out in its minimal
  form.

* We now generate a trust bundle in addition to the hashed diretory.
  This also contains only the minimal DER form of each certificate.
  This allows e.g. Unbound to preload the bundle before chrooting.

* The C version is approximately two orders of magnitude faster than the
  sh version, with rehash taking ~100 ms vs ~5-25 s depending on whether
  ca_root_nss is installed.

* We now also have tests.

Reviewed by:    kevans, markj
Differential Revision:  https://reviews.freebsd.org/D42320
Differential Revision:	https://reviews.freebsd.org/D51896
This commit is contained in:
Dag-Erling Smørgrav 2025-08-18 16:26:29 +02:00
parent a13f28d57e
commit c340ef28fd
11 changed files with 1596 additions and 411 deletions

View file

@ -1542,14 +1542,10 @@ distributeworld installworld stageworld: _installcheck_world .PHONY
.endif # make(distributeworld)
${_+_}cd ${.CURDIR}; ${IMAKE} re${.TARGET:S/world$//}; \
${IMAKEENV} rm -rf ${INSTALLTMP}
.if !make(packageworld) && ${MK_CAROOT} != "no"
@if which openssl>/dev/null; then \
PATH=${TMPPATH:Q}:${PATH:Q} \
LOCALBASE=${LOCALBASE:Q} \
sh ${SRCTOP}/usr.sbin/certctl/certctl.sh ${CERTCTLFLAGS} rehash; \
else \
echo "No openssl on the host, not rehashing certificates target -- /etc/ssl may not be populated."; \
fi
.if !make(packageworld) && ${MK_CAROOT} != "no" && ${MK_OPENSSL} != "no"
PATH=${TMPPATH:Q}:${PATH:Q} \
LOCALBASE=${LOCALBASE:Q} \
certctl ${CERTCTLFLAGS} rehash
.endif
.if make(distributeworld)
.for dist in ${EXTRA_DISTRIBUTIONS}
@ -2713,6 +2709,11 @@ _basic_bootstrap_tools+=sbin/md5
_basic_bootstrap_tools+=usr.sbin/tzsetup
.endif
# certctl is needed as an install tool
.if ${MK_CAROOT} != "no" && ${MK_OPENSSL} != "no"
_certctl=usr.sbin/certctl
.endif
.if defined(BOOTSTRAP_ALL_TOOLS)
_other_bootstrap_tools+=${_basic_bootstrap_tools}
.for _subdir _links in ${_basic_bootstrap_tools_multilink}
@ -2776,6 +2777,7 @@ bootstrap-tools: ${_bt}-links .PHONY
${_strfile} \
usr.bin/dtc \
${_cat} \
${_certctl} \
${_kbdcontrol} \
${_elftoolchain_libs} \
${_libkldelf} \

View file

@ -1255,6 +1255,8 @@
..
..
usr.sbin
certctl
..
chown
..
ctladm

View file

@ -28,7 +28,7 @@
.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
.\" SUCH DAMAGE.
.\"
.Dd October 10, 2024
.Dd August 18, 2025
.Dt HIER 7
.Os
.Sh NAME
@ -308,6 +308,21 @@ OpenSSH configuration files; see
.Xr ssh 1
.It Pa ssl/
OpenSSL configuration files
.Pp
.Bl -tag -width "untrusted/" -compact
.It Pa cert.pem
System trust store in bundle form; see
.Xr certctl 8 .
.It Pa certs/
System trust store in OpenSSL hashed-directory form; see
.Xr certctl 8 .
.It Pa openssl.cnf
OpenSSL configuration file; see
.Xr openssl.cnf 5 .
.It Pa untrusted/
Explicitly distrusted certificates; see
.Xr certctl 8 .
.El
.It Pa sysctl.conf
kernel state defaults; see
.Xr sysctl.conf 5

View file

@ -74,7 +74,7 @@
* cannot include sys/param.h and should only be updated here.
*/
#undef __FreeBSD_version
#define __FreeBSD_version 1500062
#define __FreeBSD_version 1500063
/*
* __FreeBSD_kernel__ indicates that this system uses the kernel of FreeBSD,

View file

@ -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>

View file

@ -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 .

1114
usr.sbin/certctl/certctl.c Normal file

File diff suppressed because it is too large Load diff

View file

@ -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
################################################################################

View file

@ -0,0 +1,5 @@
PACKAGE= tests
ATF_TESTS_SH= certctl_test
${PACKAGE}FILES+= certctl.subr
.include <bsd.test.mk>

View file

@ -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
}

View file

@ -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
}