xref: /freebsd/usr.sbin/certctl/certctl.sh (revision 63f537551380d2dab29fa402ad1269feae17e594)
1#!/bin/sh
2#-
3# SPDX-License-Identifier: BSD-2-Clause
4#
5# Copyright 2018 Allan Jude <allanjude@freebsd.org>
6#
7# Redistribution and use in source and binary forms, with or without
8# modification, are permitted providing that the following conditions
9# are met:
10# 1. Redistributions of source code must retain the above copyright
11#    notice, this list of conditions and the following disclaimer.
12# 2. Redistributions in binary form must reproduce the above copyright
13#    notice, this list of conditions and the following disclaimer in the
14#    documentation and/or other materials provided with the distribution.
15#
16# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
17# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
20# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
22# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
23# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
24# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
25# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26# POSSIBILITY OF SUCH DAMAGE.
27#
28
29set -u
30
31############################################################ CONFIGURATION
32
33: ${DESTDIR:=}
34: ${DISTBASE:=}
35
36############################################################ GLOBALS
37
38SCRIPTNAME="${0##*/}"
39ERRORS=0
40NOOP=false
41UNPRIV=false
42VERBOSE=false
43
44############################################################ FUNCTIONS
45
46info()
47{
48	echo "${0##*/}: $@" >&2
49}
50
51verbose()
52{
53	if "${VERBOSE}" ; then
54		info "$@"
55	fi
56}
57
58perform()
59{
60	if ! "${NOOP}" ; then
61		"$@"
62	fi
63}
64
65cert_files_in()
66{
67	find -L "$@" -type f \( \
68	     -name '*.pem' -or \
69	     -name '*.crt' -or \
70	     -name '*.cer' \
71	\) 2>/dev/null
72}
73
74do_hash()
75{
76	local hash
77
78	if hash=$(openssl x509 -noout -subject_hash -in "$1") ; then
79		echo "$hash"
80		return 0
81	else
82		info "Error: $1"
83		ERRORS=$((ERRORS + 1))
84		return 1
85	fi
86}
87
88get_decimal()
89{
90	local checkdir hash decimal
91
92	checkdir=$1
93	hash=$2
94	decimal=0
95
96	while [ -e "$checkdir/$hash.$decimal" ] ; do
97		decimal=$((decimal + 1))
98	done
99
100	echo ${decimal}
101	return 0
102}
103
104create_trusted()
105{
106	local hash certhash otherfile otherhash
107	local suffix
108	local link=${2:+-lm}
109
110	hash=$(do_hash "$1") || return
111	certhash=$(openssl x509 -sha1 -in "$1" -noout -fingerprint)
112	for otherfile in $(find $UNTRUSTDESTDIR -name "$hash.*") ; do
113		otherhash=$(openssl x509 -sha1 -in "$otherfile" -noout -fingerprint)
114		if [ "$certhash" = "$otherhash" ] ; then
115			info "Skipping untrusted certificate $hash ($otherfile)"
116			return 0
117		fi
118	done
119	for otherfile in $(find $CERTDESTDIR -name "$hash.*") ; do
120		otherhash=$(openssl x509 -sha1 -in "$otherfile" -noout -fingerprint)
121		if [ "$certhash" = "$otherhash" ] ; then
122			verbose "Skipping duplicate entry for certificate $hash"
123			return 0
124		fi
125	done
126	suffix=$(get_decimal "$CERTDESTDIR" "$hash")
127	verbose "Adding $hash.$suffix to trust store"
128	perform install ${INSTALLFLAGS} -m 0444 ${link} \
129		"$(realpath "$1")" "$CERTDESTDIR/$hash.$suffix"
130}
131
132# Accepts either dot-hash form from `certctl list` or a path to a valid cert.
133resolve_certname()
134{
135	local hash srcfile filename
136	local suffix
137
138	# If it exists as a file, we'll try that; otherwise, we'll scan
139	if [ -e "$1" ] ; then
140		hash=$(do_hash "$1") || return
141		srcfile=$(realpath "$1")
142		suffix=$(get_decimal "$UNTRUSTDESTDIR" "$hash")
143		filename="$hash.$suffix"
144		echo "$srcfile" "$hash.$suffix"
145	elif [ -e "${CERTDESTDIR}/$1" ] ;  then
146		srcfile=$(realpath "${CERTDESTDIR}/$1")
147		hash=$(echo "$1" | sed -Ee 's/\.([0-9])+$//')
148		suffix=$(get_decimal "$UNTRUSTDESTDIR" "$hash")
149		filename="$hash.$suffix"
150		echo "$srcfile" "$hash.$suffix"
151	fi
152}
153
154create_untrusted()
155{
156	local srcfile filename
157	local link=${2:+-lm}
158
159	set -- $(resolve_certname "$1")
160	srcfile=$1
161	filename=$2
162
163	if [ -z "$srcfile" -o -z "$filename" ] ; then
164		return
165	fi
166
167	verbose "Adding $filename to untrusted list"
168	perform install ${INSTALLFLAGS} -m 0444 ${link} \
169		"$srcfile" "$UNTRUSTDESTDIR/$filename"
170}
171
172do_scan()
173{
174	local CFUNC CSEARCH CPATH CFILE CERT SPLITDIR
175	local oldIFS="$IFS"
176	CFUNC="$1"
177	CSEARCH="$2"
178
179	IFS=:
180	set -- $CSEARCH
181	IFS="$oldIFS"
182	for CFILE in $(cert_files_in "$@") ; do
183		verbose "Reading $CFILE"
184		case $(egrep -c '^-+BEGIN CERTIFICATE-+$' "$CFILE") in
185		0)
186			;;
187		1)
188			"$CFUNC" "$CFILE" link
189			;;
190		*)
191			verbose "Multiple certificates found, splitting..."
192			SPLITDIR=$(mktemp -d)
193			egrep '^(---|[0-9A-Za-z/+=]+$)' "$CFILE" | \
194				split -p '^-+BEGIN CERTIFICATE-+$' - "$SPLITDIR/x"
195			for CERT in $(find "$SPLITDIR" -type f) ; do
196				"$CFUNC" "$CERT"
197			done
198			rm -rf "$SPLITDIR"
199			;;
200		esac
201	done
202}
203
204do_list()
205{
206	local CFILE subject
207
208	for CFILE in $(find "$@" \( -type f -or -type l \) -name '*.[0-9]') ; do
209		if [ ! -s "$CFILE" ] ; then
210			info "Unable to read $CFILE"
211			ERRORS=$((ERRORS + 1))
212			continue
213		fi
214		subject=
215		if ! "$VERBOSE" ; then
216			subject=$(openssl x509 -noout -subject -nameopt multiline -in "$CFILE" | sed -n '/commonName/s/.*= //p')
217		fi
218		if [ -z "$subject" ] ; then
219			subject=$(openssl x509 -noout -subject -in "$CFILE")
220		fi
221		printf "%s\t%s\n" "${CFILE##*/}" "$subject"
222	done
223}
224
225cmd_rehash()
226{
227
228	if [ -e "$CERTDESTDIR" ] ; then
229		perform find "$CERTDESTDIR" \( -type f -or -type l \) -delete
230	else
231		perform install -d -m 0755 "$CERTDESTDIR"
232	fi
233	if [ -e "$UNTRUSTDESTDIR" ] ; then
234		perform find "$UNTRUSTDESTDIR" \( -type f -or -type l \) -delete
235	else
236		perform install -d -m 0755 "$UNTRUSTDESTDIR"
237	fi
238
239	do_scan create_untrusted "$UNTRUSTPATH"
240	do_scan create_trusted "$TRUSTPATH"
241}
242
243cmd_list()
244{
245	info "Listing Trusted Certificates:"
246	do_list "$CERTDESTDIR"
247}
248
249cmd_untrust()
250{
251	local UTFILE
252
253	shift # verb
254	perform install -d -m 0755 "$UNTRUSTDESTDIR"
255	for UTFILE in "$@"; do
256		info "Adding $UTFILE to untrusted list"
257		create_untrusted "$UTFILE"
258	done
259}
260
261cmd_trust()
262{
263	local UTFILE untrustedhash certhash hash
264
265	shift # verb
266	for UTFILE in "$@"; do
267		if [ -s "$UTFILE" ] ; then
268			hash=$(do_hash "$UTFILE")
269			certhash=$(openssl x509 -sha1 -in "$UTFILE" -noout -fingerprint)
270			for UNTRUSTEDFILE in $(find $UNTRUSTDESTDIR -name "$hash.*") ; do
271				untrustedhash=$(openssl x509 -sha1 -in "$UNTRUSTEDFILE" -noout -fingerprint)
272				if [ "$certhash" = "$untrustedhash" ] ; then
273					info "Removing $(basename "$UNTRUSTEDFILE") from untrusted list"
274					perform rm -f $UNTRUSTEDFILE
275				fi
276			done
277		elif [ -e "$UNTRUSTDESTDIR/$UTFILE" ] ; then
278			info "Removing $UTFILE from untrusted list"
279			perform rm -f "$UNTRUSTDESTDIR/$UTFILE"
280		else
281			info "Cannot find $UTFILE"
282			ERRORS=$((ERRORS + 1))
283		fi
284	done
285}
286
287cmd_untrusted()
288{
289	info "Listing Untrusted Certificates:"
290	do_list "$UNTRUSTDESTDIR"
291}
292
293usage()
294{
295	exec >&2
296	echo "Manage the TLS trusted certificates on the system"
297	echo "	$SCRIPTNAME [-v] list"
298	echo "		List trusted certificates"
299	echo "	$SCRIPTNAME [-v] untrusted"
300	echo "		List untrusted certificates"
301	echo "	$SCRIPTNAME [-nUv] [-D <destdir>] [-d <distbase>] [-M <metalog>] rehash"
302	echo "		Generate hash links for all certificates"
303	echo "	$SCRIPTNAME [-nv] untrust <file>"
304	echo "		Add <file> to the list of untrusted certificates"
305	echo "	$SCRIPTNAME [-nv] trust <file>"
306	echo "		Remove <file> from the list of untrusted certificates"
307	exit 64
308}
309
310############################################################ MAIN
311
312while getopts D:d:M:nUv flag; do
313	case "$flag" in
314	D) DESTDIR=${OPTARG} ;;
315	d) DISTBASE=${OPTARG} ;;
316	M) METALOG=${OPTARG} ;;
317	n) NOOP=true ;;
318	U) UNPRIV=true ;;
319	v) VERBOSE=true ;;
320	esac
321done
322shift $((OPTIND - 1))
323
324DESTDIR=${DESTDIR%/}
325
326if ! [ -z "${CERTCTL_VERBOSE:-}" ] ; then
327	VERBOSE=true
328fi
329: ${METALOG:=${DESTDIR}/METALOG}
330INSTALLFLAGS=
331if "$UNPRIV" ; then
332	INSTALLFLAGS="-U -M ${METALOG} -D ${DESTDIR}"
333fi
334: ${LOCALBASE:=$(sysctl -n user.localbase)}
335: ${TRUSTPATH:=${DESTDIR}${DISTBASE}/usr/share/certs/trusted:${DESTDIR}${LOCALBASE}/share/certs:${DESTDIR}${LOCALBASE}/etc/ssl/certs}
336: ${UNTRUSTPATH:=${DESTDIR}${DISTBASE}/usr/share/certs/untrusted:${DESTDIR}${LOCALBASE}/etc/ssl/untrusted:${DESTDIR}${LOCALBASE}/etc/ssl/blacklisted}
337: ${CERTDESTDIR:=${DESTDIR}${DISTBASE}/etc/ssl/certs}
338: ${UNTRUSTDESTDIR:=${DESTDIR}${DISTBASE}/etc/ssl/untrusted}
339
340[ $# -gt 0 ] || usage
341case "$1" in
342list)		cmd_list ;;
343rehash)		cmd_rehash ;;
344blacklist)	cmd_untrust "$@" ;;
345untrust)	cmd_untrust "$@" ;;
346trust)		cmd_trust "$@" ;;
347unblacklist)	cmd_trust "$@" ;;
348untrusted)	cmd_untrusted ;;
349blacklisted)	cmd_untrusted ;;
350*)		usage # NOTREACHED
351esac
352
353retval=$?
354if [ $ERRORS -gt 0 ] ; then
355	info "Encountered $ERRORS errors"
356fi
357exit $retval
358
359################################################################################
360# END
361################################################################################
362