mirror of
https://git.freebsd.org/src.git
synced 2026-01-16 23:02:24 +00:00
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:
parent
a13f28d57e
commit
c340ef28fd
11 changed files with 1596 additions and 411 deletions
|
|
@ -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} \
|
||||
|
|
|
|||
|
|
@ -1255,6 +1255,8 @@
|
|||
..
|
||||
..
|
||||
usr.sbin
|
||||
certctl
|
||||
..
|
||||
chown
|
||||
..
|
||||
ctladm
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
1114
usr.sbin/certctl/certctl.c
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
################################################################################
|
||||
5
usr.sbin/certctl/tests/Makefile
Normal file
5
usr.sbin/certctl/tests/Makefile
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
PACKAGE= tests
|
||||
ATF_TESTS_SH= certctl_test
|
||||
${PACKAGE}FILES+= certctl.subr
|
||||
|
||||
.include <bsd.test.mk>
|
||||
44
usr.sbin/certctl/tests/certctl.subr
Normal file
44
usr.sbin/certctl/tests/certctl.subr
Normal 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
|
||||
}
|
||||
332
usr.sbin/certctl/tests/certctl_test.sh
Normal file
332
usr.sbin/certctl/tests/certctl_test.sh
Normal 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
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue