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