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