xref: /freebsd/usr.sbin/certctl/certctl.sh (revision 944223076fbcb681a8eb3e118ddafab79b21ac3d)
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' -or \
71	     -name '*.crl' \
72	\) 2>/dev/null
73}
74
75do_hash()
76{
77	local hash
78
79	if hash=$(openssl x509 -noout -subject_hash -in "$1") ; then
80		echo "$hash"
81		return 0
82	else
83		info "Error: $1"
84		ERRORS=$((ERRORS + 1))
85		return 1
86	fi
87}
88
89get_decimal()
90{
91	local checkdir hash decimal
92
93	checkdir=$1
94	hash=$2
95	decimal=0
96
97	while [ -e "$checkdir/$hash.$decimal" ] ; do
98		decimal=$((decimal + 1))
99	done
100
101	echo ${decimal}
102	return 0
103}
104
105create_trusted()
106{
107	local hash certhash otherfile otherhash
108	local suffix
109	local link=${2:+-lm}
110
111	hash=$(do_hash "$1") || return
112	certhash=$(openssl x509 -sha1 -in "$1" -noout -fingerprint)
113	for otherfile in $(find $UNTRUSTDESTDIR -name "$hash.*") ; do
114		otherhash=$(openssl x509 -sha1 -in "$otherfile" -noout -fingerprint)
115		if [ "$certhash" = "$otherhash" ] ; then
116			info "Skipping untrusted certificate $hash ($otherfile)"
117			return 1
118		fi
119	done
120	for otherfile in $(find $CERTDESTDIR -name "$hash.*") ; do
121		otherhash=$(openssl x509 -sha1 -in "$otherfile" -noout -fingerprint)
122		if [ "$certhash" = "$otherhash" ] ; then
123			verbose "Skipping duplicate entry for certificate $hash"
124			return 0
125		fi
126	done
127	suffix=$(get_decimal "$CERTDESTDIR" "$hash")
128	verbose "Adding $hash.$suffix to trust store"
129	perform install ${INSTALLFLAGS} -m 0444 ${link} \
130		"$(realpath "$1")" "$CERTDESTDIR/$hash.$suffix"
131}
132
133# Accepts either dot-hash form from `certctl list` or a path to a valid cert.
134resolve_certname()
135{
136	local hash srcfile filename
137	local suffix
138
139	# If it exists as a file, we'll try that; otherwise, we'll scan
140	if [ -e "$1" ] ; then
141		hash=$(do_hash "$1") || return
142		srcfile=$(realpath "$1")
143		suffix=$(get_decimal "$UNTRUSTDESTDIR" "$hash")
144		filename="$hash.$suffix"
145		echo "$srcfile" "$hash.$suffix"
146	elif [ -e "${CERTDESTDIR}/$1" ] ;  then
147		srcfile=$(realpath "${CERTDESTDIR}/$1")
148		hash=$(echo "$1" | sed -Ee 's/\.([0-9])+$//')
149		suffix=$(get_decimal "$UNTRUSTDESTDIR" "$hash")
150		filename="$hash.$suffix"
151		echo "$srcfile" "$hash.$suffix"
152	fi
153}
154
155create_untrusted()
156{
157	local srcfile filename
158	local link=${2:+-lm}
159
160	set -- $(resolve_certname "$1")
161	srcfile=$1
162	filename=$2
163
164	if [ -z "$srcfile" -o -z "$filename" ] ; then
165		return
166	fi
167
168	verbose "Adding $filename to untrusted list"
169	perform install ${INSTALLFLAGS} -m 0444 ${link} \
170		"$srcfile" "$UNTRUSTDESTDIR/$filename"
171}
172
173do_scan()
174{
175	local CFUNC CSEARCH CPATH CFILE CERT SPLITDIR
176	local oldIFS="$IFS"
177	CFUNC="$1"
178	CSEARCH="$2"
179
180	IFS=:
181	set -- $CSEARCH
182	IFS="$oldIFS"
183	for CFILE in $(cert_files_in "$@") ; do
184		verbose "Reading $CFILE"
185		case $(grep -c '^Certificate:$' "$CFILE") in
186		0)
187			;;
188		1)
189			"$CFUNC" "$CFILE" link
190			;;
191		*)
192			verbose "Multiple certificates found, splitting..."
193			SPLITDIR=$(mktemp -d)
194			egrep '^[^#]' "$CFILE" | \
195				split -p '^Certificate:$' - "$SPLITDIR/x"
196			for CERT in $(find "$SPLITDIR" -type f) ; do
197				"$CFUNC" "$CERT"
198			done
199			rm -rf "$SPLITDIR"
200			;;
201		esac
202	done
203}
204
205do_list()
206{
207	local CFILE subject
208
209	for CFILE in $(find "$@" \( -type f -or -type l \) -name '*.[0-9]') ; do
210		if [ ! -s "$CFILE" ] ; then
211			info "Unable to read $CFILE"
212			ERRORS=$((ERRORS + 1))
213			continue
214		fi
215		subject=
216		if ! "$VERBOSE" ; then
217			subject=$(openssl x509 -noout -subject -nameopt multiline -in "$CFILE" | sed -n '/commonName/s/.*= //p')
218		fi
219		if [ -z "$subject" ] ; then
220			subject=$(openssl x509 -noout -subject -in "$CFILE")
221		fi
222		printf "%s\t%s\n" "${CFILE##*/}" "$subject"
223	done
224}
225
226cmd_rehash()
227{
228
229	if [ -e "$CERTDESTDIR" ] ; then
230		perform find "$CERTDESTDIR" \( -type f -or -type l \) -delete
231	else
232		perform install -d -m 0755 "$CERTDESTDIR"
233	fi
234	if [ -e "$UNTRUSTDESTDIR" ] ; then
235		perform find "$UNTRUSTDESTDIR" \( -type f -or -type l \) -delete
236	else
237		perform install -d -m 0755 "$UNTRUSTDESTDIR"
238	fi
239
240	do_scan create_untrusted "$UNTRUSTPATH"
241	do_scan create_trusted "$TRUSTPATH"
242}
243
244cmd_list()
245{
246	info "Listing Trusted Certificates:"
247	do_list "$CERTDESTDIR"
248}
249
250cmd_untrust()
251{
252	local UTFILE
253
254	shift # verb
255	perform install -d -m 0755 "$UNTRUSTDESTDIR"
256	for UTFILE in "$@"; do
257		info "Adding $UTFILE to untrusted list"
258		create_untrusted "$UTFILE"
259	done
260}
261
262cmd_trust()
263{
264	local UTFILE untrustedhash certhash hash
265
266	shift # verb
267	for UTFILE in "$@"; do
268		if [ -s "$UTFILE" ] ; then
269			hash=$(do_hash "$UTFILE")
270			certhash=$(openssl x509 -sha1 -in "$UTFILE" -noout -fingerprint)
271			for UNTRUSTEDFILE in $(find $UNTRUSTDESTDIR -name "$hash.*") ; do
272				untrustedhash=$(openssl x509 -sha1 -in "$UNTRUSTEDFILE" -noout -fingerprint)
273				if [ "$certhash" = "$untrustedhash" ] ; then
274					info "Removing $(basename "$UNTRUSTEDFILE") from untrusted list"
275					perform rm -f $UNTRUSTEDFILE
276				fi
277			done
278		elif [ -e "$UNTRUSTDESTDIR/$UTFILE" ] ; then
279			info "Removing $UTFILE from untrusted list"
280			perform rm -f "$UNTRUSTDESTDIR/$UTFILE"
281		else
282			info "Cannot find $UTFILE"
283			ERRORS=$((ERRORS + 1))
284		fi
285	done
286}
287
288cmd_untrusted()
289{
290	info "Listing Untrusted Certificates:"
291	do_list "$UNTRUSTDESTDIR"
292}
293
294usage()
295{
296	exec >&2
297	echo "Manage the TLS trusted certificates on the system"
298	echo "	$SCRIPTNAME [-v] list"
299	echo "		List trusted certificates"
300	echo "	$SCRIPTNAME [-v] untrusted"
301	echo "		List untrusted certificates"
302	echo "	$SCRIPTNAME [-nUv] [-D <destdir>] [-d <distbase>] [-M <metalog>] rehash"
303	echo "		Generate hash links for all certificates"
304	echo "	$SCRIPTNAME [-nv] untrust <file>"
305	echo "		Add <file> to the list of untrusted certificates"
306	echo "	$SCRIPTNAME [-nv] trust <file>"
307	echo "		Remove <file> from the list of untrusted certificates"
308	exit 64
309}
310
311############################################################ MAIN
312
313while getopts D:d:M:nUv flag; do
314	case "$flag" in
315	D) DESTDIR=${OPTARG} ;;
316	d) DISTBASE=${OPTARG} ;;
317	M) METALOG=${OPTARG} ;;
318	n) NOOP=true ;;
319	U) UNPRIV=true ;;
320	v) VERBOSE=true ;;
321	esac
322done
323shift $((OPTIND - 1))
324
325DESTDIR=${DESTDIR%/}
326
327if ! [ -z "${CERTCTL_VERBOSE:-}" ] ; then
328	VERBOSE=true
329fi
330: ${METALOG:=${DESTDIR}/METALOG}
331INSTALLFLAGS=
332if "$UNPRIV" ; then
333	INSTALLFLAGS="-U -M ${METALOG} -D ${DESTDIR}"
334fi
335: ${LOCALBASE:=$(sysctl -n user.localbase)}
336: ${TRUSTPATH:=${DESTDIR}${DISTBASE}/usr/share/certs/trusted:${DESTDIR}${LOCALBASE}/share/certs:${DESTDIR}${LOCALBASE}/etc/ssl/certs}
337: ${UNTRUSTPATH:=${DESTDIR}${DISTBASE}/usr/share/certs/untrusted:${DESTDIR}${LOCALBASE}/etc/ssl/untrusted:${DESTDIR}${LOCALBASE}/etc/ssl/blacklisted}
338: ${CERTDESTDIR:=${DESTDIR}${DISTBASE}/etc/ssl/certs}
339: ${UNTRUSTDESTDIR:=${DESTDIR}${DISTBASE}/etc/ssl/untrusted}
340
341[ $# -gt 0 ] || usage
342case "$1" in
343list)		cmd_list ;;
344rehash)		cmd_rehash ;;
345blacklist)	cmd_untrust "$@" ;;
346untrust)	cmd_untrust "$@" ;;
347trust)		cmd_trust "$@" ;;
348unblacklist)	cmd_trust "$@" ;;
349untrusted)	cmd_untrusted ;;
350blacklisted)	cmd_untrusted ;;
351*)		usage # NOTREACHED
352esac
353
354retval=$?
355if [ $ERRORS -gt 0 ] ; then
356	info "Encountered $ERRORS errors"
357fi
358exit $retval
359
360################################################################################
361# END
362################################################################################
363