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