1#!/bin/sh 2#- 3# SPDX-License-Identifier: BSD-2-Clause-FreeBSD 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# $FreeBSD$ 29 30############################################################ CONFIGURATION 31 32: ${DESTDIR:=} 33: ${FILEPAT:="\.pem$|\.crt$|\.cer$|\.crl$"} 34: ${VERBOSE:=0} 35 36############################################################ GLOBALS 37 38SCRIPTNAME="${0##*/}" 39ERRORS=0 40NOOP=0 41UNPRIV=0 42 43############################################################ FUNCTIONS 44 45do_hash() 46{ 47 local hash 48 49 if hash=$( openssl x509 -noout -subject_hash -in "$1" ); then 50 echo "$hash" 51 return 0 52 else 53 echo "Error: $1" >&2 54 ERRORS=$(( $ERRORS + 1 )) 55 return 1 56 fi 57} 58 59get_decimal() 60{ 61 local checkdir hash decimal 62 63 checkdir=$1 64 hash=$2 65 decimal=0 66 67 while [ -e "$checkdir/$hash.$decimal" ]; do 68 decimal=$((decimal + 1)) 69 done 70 71 echo ${decimal} 72 return 0 73} 74 75create_trusted_link() 76{ 77 local blisthash certhash hash 78 local suffix 79 80 hash=$( do_hash "$1" ) || return 81 certhash=$( openssl x509 -sha1 -in "$1" -noout -fingerprint ) 82 for blistfile in $(find $BLACKLISTDESTDIR -name "$hash.*"); do 83 blisthash=$( openssl x509 -sha1 -in "$blistfile" -noout -fingerprint ) 84 if [ "$certhash" = "$blisthash" ]; then 85 echo "Skipping blacklisted certificate $1 ($blistfile)" 86 return 1 87 fi 88 done 89 suffix=$(get_decimal "$CERTDESTDIR" "$hash") 90 [ $VERBOSE -gt 0 ] && echo "Adding $hash.$suffix to trust store" 91 [ $NOOP -eq 0 ] && \ 92 install ${INSTALLFLAGS} -lrs $(realpath "$1") "$CERTDESTDIR/$hash.$suffix" 93} 94 95# Accepts either dot-hash form from `certctl list` or a path to a valid cert. 96resolve_certname() 97{ 98 local hash srcfile filename 99 local suffix 100 101 # If it exists as a file, we'll try that; otherwise, we'll scan 102 if [ -e "$1" ]; then 103 hash=$( do_hash "$1" ) || return 104 srcfile=$(realpath "$1") 105 suffix=$(get_decimal "$BLACKLISTDESTDIR" "$hash") 106 filename="$hash.$suffix" 107 echo "$srcfile" "$hash.$suffix" 108 elif [ -e "${CERTDESTDIR}/$1" ]; then 109 srcfile=$(realpath "${CERTDESTDIR}/$1") 110 hash=$(echo "$1" | sed -Ee 's/\.([0-9])+$//') 111 suffix=$(get_decimal "$BLACKLISTDESTDIR" "$hash") 112 filename="$hash.$suffix" 113 echo "$srcfile" "$hash.$suffix" 114 fi 115} 116 117create_blacklisted() 118{ 119 local srcfile filename 120 121 set -- $(resolve_certname "$1") 122 srcfile=$1 123 filename=$2 124 125 if [ -z "$srcfile" -o -z "$filename" ]; then 126 return 127 fi 128 129 [ $VERBOSE -gt 0 ] && echo "Adding $filename to blacklist" 130 [ $NOOP -eq 0 ] && install ${INSTALLFLAGS} -lrs "$srcfile" "$BLACKLISTDESTDIR/$filename" 131} 132 133do_scan() 134{ 135 local CFUNC CSEARCH CPATH CFILE 136 local oldIFS="$IFS" 137 CFUNC="$1" 138 CSEARCH="$2" 139 140 IFS=: 141 set -- $CSEARCH 142 IFS="$oldIFS" 143 for CPATH in "$@"; do 144 [ -d "$CPATH" ] || continue 145 echo "Scanning $CPATH for certificates..." 146 for CFILE in $(ls -1 "${CPATH}" | grep -Ee "${FILEPAT}"); do 147 [ -e "$CPATH/$CFILE" ] || continue 148 [ $VERBOSE -gt 0 ] && echo "Reading $CFILE" 149 "$CFUNC" "$CPATH/$CFILE" 150 done 151 done 152} 153 154do_list() 155{ 156 local CFILE subject 157 158 if [ -e "$1" ]; then 159 cd "$1" 160 for CFILE in *.[0-9]; do 161 if [ ! -s "$CFILE" ]; then 162 echo "Unable to read $CFILE" >&2 163 ERRORS=$(( $ERRORS + 1 )) 164 continue 165 fi 166 subject= 167 if [ $VERBOSE -eq 0 ]; then 168 subject=$( openssl x509 -noout -subject -nameopt multiline -in "$CFILE" | 169 sed -n '/commonName/s/.*= //p' ) 170 fi 171 [ "$subject" ] || 172 subject=$( openssl x509 -noout -subject -in "$CFILE" ) 173 printf "%s\t%s\n" "$CFILE" "$subject" 174 done 175 cd - 176 fi 177} 178 179cmd_rehash() 180{ 181 182 if [ $NOOP -eq 0 ]; then 183 if [ -e "$CERTDESTDIR" ]; then 184 find "$CERTDESTDIR" -type link -delete 185 else 186 mkdir -p "$CERTDESTDIR" 187 fi 188 if [ -e "$BLACKLISTDESTDIR" ]; then 189 find "$BLACKLISTDESTDIR" -type link -delete 190 else 191 mkdir -p "$BLACKLISTDESTDIR" 192 fi 193 fi 194 195 do_scan create_blacklisted "$BLACKLISTPATH" 196 do_scan create_trusted_link "$TRUSTPATH" 197} 198 199cmd_list() 200{ 201 echo "Listing Trusted Certificates:" 202 do_list "$CERTDESTDIR" 203} 204 205cmd_blacklist() 206{ 207 local BPATH 208 209 shift # verb 210 [ $NOOP -eq 0 ] && mkdir -p "$BLACKLISTDESTDIR" 211 for BFILE in "$@"; do 212 echo "Adding $BFILE to blacklist" 213 create_blacklisted "$BFILE" 214 done 215} 216 217cmd_unblacklist() 218{ 219 local BFILE blisthash certhash hash 220 221 shift # verb 222 for BFILE in "$@"; do 223 if [ -s "$BFILE" ]; then 224 hash=$( do_hash "$BFILE" ) 225 certhash=$( openssl x509 -sha1 -in "$BFILE" -noout -fingerprint ) 226 for BLISTEDFILE in $(find $BLACKLISTDESTDIR -name "$hash.*"); do 227 blisthash=$( openssl x509 -sha1 -in "$BLISTEDFILE" -noout -fingerprint ) 228 if [ "$certhash" = "$blisthash" ]; then 229 echo "Removing $(basename "$BLISTEDFILE") from blacklist" 230 [ $NOOP -eq 0 ] && rm -f $BLISTEDFILE 231 fi 232 done 233 elif [ -e "$BLACKLISTDESTDIR/$BFILE" ]; then 234 echo "Removing $BFILE from blacklist" 235 [ $NOOP -eq 0 ] && rm -f "$BLACKLISTDESTDIR/$BFILE" 236 else 237 echo "Cannot find $BFILE" >&2 238 ERRORS=$(( $ERRORS + 1 )) 239 fi 240 done 241} 242 243cmd_blacklisted() 244{ 245 echo "Listing Blacklisted Certificates:" 246 do_list "$BLACKLISTDESTDIR" 247} 248 249usage() 250{ 251 exec >&2 252 echo "Manage the TLS trusted certificates on the system" 253 echo " $SCRIPTNAME [-v] list" 254 echo " List trusted certificates" 255 echo " $SCRIPTNAME [-v] blacklisted" 256 echo " List blacklisted certificates" 257 echo " $SCRIPTNAME [-nUv] [-D <destdir>] [-M <metalog>] rehash" 258 echo " Generate hash links for all certificates" 259 echo " $SCRIPTNAME [-nv] blacklist <file>" 260 echo " Add <file> to the list of blacklisted certificates" 261 echo " $SCRIPTNAME [-nv] unblacklist <file>" 262 echo " Remove <file> from the list of blacklisted certificates" 263 exit 64 264} 265 266############################################################ MAIN 267 268while getopts D:M:nUv flag; do 269 case "$flag" in 270 D) DESTDIR=${OPTARG} ;; 271 M) METALOG=${OPTARG} ;; 272 n) NOOP=1 ;; 273 U) UNPRIV=1 ;; 274 v) VERBOSE=$(( $VERBOSE + 1 )) ;; 275 esac 276done 277shift $(( $OPTIND - 1 )) 278 279: ${METALOG:=${DESTDIR}/METALOG} 280INSTALLFLAGS= 281[ $UNPRIV -eq 1 ] && INSTALLFLAGS="-U -M ${METALOG} -D ${DESTDIR}" 282: ${LOCALBASE:=$(sysctl -n user.localbase)} 283: ${TRUSTPATH:=${DESTDIR}/usr/share/certs/trusted:${DESTDIR}${LOCALBASE}/share/certs:${DESTDIR}${LOCALBASE}/etc/ssl/certs} 284: ${BLACKLISTPATH:=${DESTDIR}/usr/share/certs/blacklisted:${DESTDIR}${LOCALBASE}/etc/ssl/blacklisted} 285: ${CERTDESTDIR:=${DESTDIR}/etc/ssl/certs} 286: ${BLACKLISTDESTDIR:=${DESTDIR}/etc/ssl/blacklisted} 287 288[ $# -gt 0 ] || usage 289case "$1" in 290list) cmd_list ;; 291rehash) cmd_rehash ;; 292blacklist) cmd_blacklist "$@" ;; 293unblacklist) cmd_unblacklist "$@" ;; 294blacklisted) cmd_blacklisted ;; 295*) usage # NOTREACHED 296esac 297 298retval=$? 299[ $ERRORS -gt 0 ] && echo "Encountered $ERRORS errors" >&2 300exit $retval 301 302################################################################################ 303# END 304################################################################################ 305