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. * 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. * The DISTBASE concept has been dropped; the same effect can be achieved by adjusting DESTDIR. * We now also have rudimentary tests. Reviewed by: kevans Differential Revision: https://reviews.freebsd.org/D42320
This commit is contained in:
+9
-8
@@ -1021,8 +1021,7 @@ IMAKE_MTREE= MTREE_CMD="${MTREE_CMD} ${MTREEFLAGS}"
|
|||||||
.endif
|
.endif
|
||||||
|
|
||||||
.if make(distributeworld)
|
.if make(distributeworld)
|
||||||
CERTCTLDESTDIR= ${DESTDIR}/${DISTDIR}
|
CERTCTLDESTDIR= ${DESTDIR}/${DISTDIR}/base
|
||||||
CERTCTLFLAGS+= -d /base
|
|
||||||
.else
|
.else
|
||||||
CERTCTLDESTDIR= ${DESTDIR}
|
CERTCTLDESTDIR= ${DESTDIR}
|
||||||
.endif
|
.endif
|
||||||
@@ -1541,14 +1540,10 @@ distributeworld installworld stageworld: _installcheck_world .PHONY
|
|||||||
.endif # make(distributeworld)
|
.endif # make(distributeworld)
|
||||||
${_+_}cd ${.CURDIR}; ${IMAKE} re${.TARGET:S/world$//}; \
|
${_+_}cd ${.CURDIR}; ${IMAKE} re${.TARGET:S/world$//}; \
|
||||||
${IMAKEENV} rm -rf ${INSTALLTMP}
|
${IMAKEENV} rm -rf ${INSTALLTMP}
|
||||||
.if !make(packageworld) && ${MK_CAROOT} != "no"
|
.if !make(packageworld) && ${MK_CAROOT} != "no" && ${MK_OPENSSL} != "no"
|
||||||
@if which openssl>/dev/null; then \
|
|
||||||
PATH=${TMPPATH:Q}:${PATH:Q} \
|
PATH=${TMPPATH:Q}:${PATH:Q} \
|
||||||
LOCALBASE=${LOCALBASE:Q} \
|
LOCALBASE=${LOCALBASE:Q} \
|
||||||
sh ${SRCTOP}/usr.sbin/certctl/certctl.sh ${CERTCTLFLAGS} rehash; \
|
certctl ${CERTCTLFLAGS} rehash
|
||||||
else \
|
|
||||||
echo "No openssl on the host, not rehashing certificates target -- /etc/ssl may not be populated."; \
|
|
||||||
fi
|
|
||||||
.endif
|
.endif
|
||||||
.if make(distributeworld)
|
.if make(distributeworld)
|
||||||
.for dist in ${EXTRA_DISTRIBUTIONS}
|
.for dist in ${EXTRA_DISTRIBUTIONS}
|
||||||
@@ -2712,6 +2707,11 @@ _basic_bootstrap_tools+=sbin/md5
|
|||||||
_basic_bootstrap_tools+=usr.sbin/tzsetup
|
_basic_bootstrap_tools+=usr.sbin/tzsetup
|
||||||
.endif
|
.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)
|
.if defined(BOOTSTRAP_ALL_TOOLS)
|
||||||
_other_bootstrap_tools+=${_basic_bootstrap_tools}
|
_other_bootstrap_tools+=${_basic_bootstrap_tools}
|
||||||
.for _subdir _links in ${_basic_bootstrap_tools_multilink}
|
.for _subdir _links in ${_basic_bootstrap_tools_multilink}
|
||||||
@@ -2775,6 +2775,7 @@ bootstrap-tools: ${_bt}-links .PHONY
|
|||||||
${_strfile} \
|
${_strfile} \
|
||||||
usr.bin/dtc \
|
usr.bin/dtc \
|
||||||
${_cat} \
|
${_cat} \
|
||||||
|
${_certctl} \
|
||||||
${_kbdcontrol} \
|
${_kbdcontrol} \
|
||||||
${_elftoolchain_libs} \
|
${_elftoolchain_libs} \
|
||||||
${_libkldelf} \
|
${_libkldelf} \
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
|
.include <src.opts.mk>
|
||||||
|
|
||||||
PACKAGE= certctl
|
PACKAGE= certctl
|
||||||
SCRIPTS=certctl.sh
|
PROG= certctl
|
||||||
MAN= certctl.8
|
MAN= certctl.8
|
||||||
|
LIBADD= crypto
|
||||||
|
HAS_TESTS=
|
||||||
|
SUBDIR.${MK_TESTS}= tests
|
||||||
|
|
||||||
.include <bsd.prog.mk>
|
.include <bsd.prog.mk>
|
||||||
|
|||||||
+57
-37
@@ -24,7 +24,7 @@
|
|||||||
.\" IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
.\" IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
.\" POSSIBILITY OF SUCH DAMAGE.
|
.\" POSSIBILITY OF SUCH DAMAGE.
|
||||||
.\"
|
.\"
|
||||||
.Dd July 17, 2025
|
.Dd August 11, 2025
|
||||||
.Dt CERTCTL 8
|
.Dt CERTCTL 8
|
||||||
.Os
|
.Os
|
||||||
.Sh NAME
|
.Sh NAME
|
||||||
@@ -32,63 +32,83 @@
|
|||||||
.Nd "tool for managing trusted and untrusted TLS certificates"
|
.Nd "tool for managing trusted and untrusted TLS certificates"
|
||||||
.Sh SYNOPSIS
|
.Sh SYNOPSIS
|
||||||
.Nm
|
.Nm
|
||||||
.Op Fl v
|
.Op Fl lv
|
||||||
.Ic list
|
.Ic list
|
||||||
.Nm
|
.Nm
|
||||||
.Op Fl v
|
.Op Fl lv
|
||||||
.Ic untrusted
|
.Ic untrusted
|
||||||
.Nm
|
.Nm
|
||||||
.Op Fl cnUv
|
.Op Fl BnUv
|
||||||
.Op Fl D Ar destdir
|
.Op Fl D Ar destdir
|
||||||
.Op Fl M Ar metalog
|
.Op Fl M Ar metalog
|
||||||
.Ic rehash
|
.Ic rehash
|
||||||
.Nm
|
.Nm
|
||||||
.Op Fl cnv
|
.Op Fl nv
|
||||||
.Ic untrust Ar file
|
.Ic untrust Ar
|
||||||
.Nm
|
.Nm
|
||||||
.Op Fl cnv
|
.Op Fl nv
|
||||||
.Ic trust Ar file
|
.Ic trust Ar
|
||||||
.Sh DESCRIPTION
|
.Sh DESCRIPTION
|
||||||
The
|
The
|
||||||
.Nm
|
.Nm
|
||||||
utility manages the list of TLS Certificate Authorities that are trusted by
|
utility manages the list of TLS Certificate Authorities that are trusted by
|
||||||
applications that use OpenSSL.
|
applications that use OpenSSL.
|
||||||
.Pp
|
.Pp
|
||||||
Flags:
|
The following options are available:
|
||||||
.Bl -tag -width 4n
|
.Bl -tag -width 4n
|
||||||
.It Fl c
|
.It Fl B
|
||||||
Copy certificates instead of linking to them.
|
Do not generate a bundle.
|
||||||
|
This option is only valid in conjunction with the
|
||||||
|
.Ic rehash
|
||||||
|
command.
|
||||||
.It Fl D Ar destdir
|
.It Fl D Ar destdir
|
||||||
Specify the DESTDIR (overriding values from the environment).
|
Specify the DESTDIR (overriding values from the environment).
|
||||||
.It Fl d Ar distbase
|
.It Fl l
|
||||||
Specify the DISTBASE (overriding values from the environment).
|
When listing installed (trusted or untrusted) certificates, show the
|
||||||
|
full path and distinguished name for each certificate.
|
||||||
.It Fl M Ar metalog
|
.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
|
.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
|
.It Fl v
|
||||||
Be verbose, print details about actions before performing them.
|
Verbose mode.
|
||||||
|
Print detailed information about each action taken.
|
||||||
.It Fl U
|
.It Fl U
|
||||||
Unprivileged mode, do not change the ownership of created links.
|
Unprivileged mode.
|
||||||
Do record the ownership in the METALOG file.
|
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
|
.El
|
||||||
.Pp
|
.Pp
|
||||||
Primary command functions:
|
Primary command functions:
|
||||||
.Bl -tag -width untrusted
|
.Bl -tag -width untrusted
|
||||||
.It Ic list
|
.It Ic list
|
||||||
List all currently trusted certificate authorities.
|
List all currently trusted certificates.
|
||||||
.It Ic untrusted
|
.It Ic untrusted
|
||||||
List all currently untrusted certificates.
|
List all currently untrusted certificates.
|
||||||
.It Ic rehash
|
.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
|
in
|
||||||
.Ev TRUSTPATH
|
.Ev TRUSTPATH
|
||||||
and all untrusted certificates in
|
and all untrusted certificates in
|
||||||
.Ev UNTRUSTPATH .
|
.Ev UNTRUSTPATH .
|
||||||
A symbolic link to each trusted certificate is placed in
|
A copy of each trusted certificate is placed in
|
||||||
.Ev CERTDESTDIR
|
.Ev CERTDESTDIR
|
||||||
and each untrusted certificate in
|
and each untrusted certificate in
|
||||||
.Ev UNTRUSTDESTDIR .
|
.Ev UNTRUSTDESTDIR .
|
||||||
|
In addition, a bundle containing the trusted certificates is placed in
|
||||||
|
.Ev BUNDLEFILE .
|
||||||
.It Ic untrust
|
.It Ic untrust
|
||||||
Add the specified file to the untrusted list.
|
Add the specified file to the untrusted list.
|
||||||
.It Ic trust
|
.It Ic trust
|
||||||
@@ -98,8 +118,6 @@ Remove the specified file from the untrusted list.
|
|||||||
.Bl -tag -width UNTRUSTDESTDIR
|
.Bl -tag -width UNTRUSTDESTDIR
|
||||||
.It Ev DESTDIR
|
.It Ev DESTDIR
|
||||||
Alternate destination directory to operate on.
|
Alternate destination directory to operate on.
|
||||||
.It Ev DISTBASE
|
|
||||||
Additional path component to include when operating on certificate directories.
|
|
||||||
.It Ev LOCALBASE
|
.It Ev LOCALBASE
|
||||||
Location for local programs.
|
Location for local programs.
|
||||||
Defaults to the value of the user.localbase sysctl which is usually
|
Defaults to the value of the user.localbase sysctl which is usually
|
||||||
@@ -107,32 +125,34 @@ Defaults to the value of the user.localbase sysctl which is usually
|
|||||||
.It Ev TRUSTPATH
|
.It Ev TRUSTPATH
|
||||||
List of paths to search for trusted certificates.
|
List of paths to search for trusted certificates.
|
||||||
Default:
|
Default:
|
||||||
.Pa <DESTDIR><DISTBASE>/usr/share/certs/trusted
|
.Pa ${DESTDIR}/usr/share/certs/trusted
|
||||||
.Pa <DESTDIR><DISTBASE>/usr/local/share/certs
|
.Pa ${DESTDIR}${LOCALBASE}/share/certs/trusted
|
||||||
.Pa <DESTDIR><DISTBASE><LOCALBASE>/etc/ssl/certs
|
.Pa ${DESTDIR}${LOCALBASE}/share/certs
|
||||||
.It Ev UNTRUSTPATH
|
.It Ev UNTRUSTPATH
|
||||||
List of paths to search for untrusted certificates.
|
List of paths to search for untrusted certificates.
|
||||||
Default:
|
Default:
|
||||||
.Pa <DESTDIR><DISTBASE>/usr/share/certs/untrusted
|
.Pa ${DESTDIR}/usr/share/certs/untrusted
|
||||||
.Pa <DESTDIR><DISTBASE><LOCALBASE>/etc/ssl/untrusted
|
.Pa ${DESTDIR}${LOCALBASE}/share/certs/untrusted
|
||||||
.Pa <DESTDIR><DISTBASE><LOCALBASE>/etc/ssl/blacklisted
|
.It Ev TRUSTDESTDIR
|
||||||
.It Ev CERTDESTDIR
|
|
||||||
Destination directory for symbolic links to trusted certificates.
|
Destination directory for symbolic links to trusted certificates.
|
||||||
Default:
|
Default:
|
||||||
.Pa <DESTDIR><DISTBASE>/etc/ssl/certs
|
.Pa ${DESTDIR}/etc/ssl/certs
|
||||||
.It Ev UNTRUSTDESTDIR
|
.It Ev UNTRUSTDESTDIR
|
||||||
Destination directory for symbolic links to untrusted certificates.
|
Destination directory for symbolic links to untrusted certificates.
|
||||||
Default:
|
Default:
|
||||||
.Pa <DESTDIR><DISTBASE>/etc/ssl/untrusted
|
.Pa ${DESTDIR}/etc/ssl/untrusted
|
||||||
.It Ev EXTENSIONS
|
.It Ev BUNDLE
|
||||||
List of file extensions to read as certificate files.
|
File name of bundle to produce.
|
||||||
Default: *.pem *.crt *.cer *.crl *.0
|
|
||||||
.El
|
.El
|
||||||
.Sh SEE ALSO
|
.Sh SEE ALSO
|
||||||
.Xr openssl 1
|
.Xr openssl 1
|
||||||
.Sh HISTORY
|
.Sh HISTORY
|
||||||
.Nm
|
.Nm
|
||||||
first appeared in
|
first appeared in
|
||||||
.Fx 12.2
|
.Fx 12.2 .
|
||||||
.Sh AUTHORS
|
.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 .
|
||||||
|
|||||||
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
|
|
||||||
################################################################################
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
PACKAGE= tests
|
||||||
|
ATF_TESTS_SH= certctl_test
|
||||||
|
${PACKAGE}FILES+= certctl.subr
|
||||||
|
|
||||||
|
.include <bsd.test.mk>
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
#
|
||||||
|
# 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# Create output directories
|
||||||
|
mkdir -p etc/ssl/certs
|
||||||
|
mkdir -p etc/ssl/untrusted
|
||||||
|
|
||||||
|
# Generate a random key
|
||||||
|
keyname="testkey"
|
||||||
|
gen_key ${keyname}
|
||||||
|
|
||||||
|
# Generate certificates
|
||||||
|
set1 | while read crtname hash ; do
|
||||||
|
gen_crt ${crtname} ${keyname}
|
||||||
|
mv ${crtname}.crt usr/share/certs/trusted
|
||||||
|
done
|
||||||
|
coll | while read crtname hash ; do
|
||||||
|
gen_crt ${crtname} ${keyname}
|
||||||
|
mv ${crtname}.crt usr/share/certs/trusted
|
||||||
|
done
|
||||||
|
set2 | while read crtname hash ; do
|
||||||
|
gen_crt ${crtname} ${keyname}
|
||||||
|
openssl x509 -in ${crtname}.crt
|
||||||
|
rm ${crtname}.crt
|
||||||
|
done >usr/local/share/certs/bundle.crt
|
||||||
|
set3 | while read crtname hash ; do
|
||||||
|
gen_crt ${crtname} ${keyname}
|
||||||
|
mv ${crtname}.crt usr/share/certs/untrusted
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
check_trusted() {
|
||||||
|
local crtname=$1
|
||||||
|
local subject="$(subject ${crtname})"
|
||||||
|
local c=${2:-1}
|
||||||
|
|
||||||
|
atf_check -o match:"found: ${c}\$" \
|
||||||
|
openssl storeutl -noout -subject "${subject}" \
|
||||||
|
etc/ssl/certs
|
||||||
|
atf_check -o match:"found: 0\$" \
|
||||||
|
openssl storeutl -noout -subject "${subject}" \
|
||||||
|
etc/ssl/untrusted
|
||||||
|
}
|
||||||
|
|
||||||
|
check_untrusted() {
|
||||||
|
local crtname=$1
|
||||||
|
local subject="$(subject ${crtname})"
|
||||||
|
local c=${2:-1}
|
||||||
|
|
||||||
|
atf_check -o match:"found: 0\$" \
|
||||||
|
openssl storeutl -noout -subject "${subject}" \
|
||||||
|
etc/ssl/certs
|
||||||
|
atf_check -o match:"found: ${c}\$" \
|
||||||
|
openssl storeutl -noout -subject "${subject}" \
|
||||||
|
etc/ssl/untrusted
|
||||||
|
}
|
||||||
|
|
||||||
|
check_in_bundle() {
|
||||||
|
local crtfile=$1
|
||||||
|
local line
|
||||||
|
|
||||||
|
line=$(tail +5 "${crtfile}" | head -1)
|
||||||
|
atf_check grep -q "${line}" etc/ssl/cert.pem
|
||||||
|
}
|
||||||
|
|
||||||
|
check_not_in_bundle() {
|
||||||
|
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; storeutl is no help here
|
||||||
|
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 trust
|
||||||
|
trust_head()
|
||||||
|
{
|
||||||
|
atf_set "descr" "Test the trust command"
|
||||||
|
}
|
||||||
|
trust_body()
|
||||||
|
{
|
||||||
|
certctl_setup
|
||||||
|
atf_check certctl rehash
|
||||||
|
crtname=NJWIRLPWAIICVJBKXXHFHLCPAERZATRL
|
||||||
|
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=AVOYKJHSLFHWPVQMKBHENUAHJTEGMCCB
|
||||||
|
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_init_test_cases()
|
||||||
|
{
|
||||||
|
atf_add_test_case rehash
|
||||||
|
atf_add_test_case trust
|
||||||
|
atf_add_test_case untrust
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user