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