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