#!/bin/bash # trivial check if server cert is OK incl. best effort to download # referenced certificates and CRLs in chain # # jpokorny@redhat.com # # TODO: # - currently, only cl[tl] files supported, not immediate PEM etc.; # also any reference to external resource has to start with URI # (is it a convention or a single case?) # - couldn't get rid of dependency on temporary file as it is read # twice in two substituted commands and neither env. variable nor # file descriptor sharing is suitable (stdin can be read only once, # generally, there is a race between the two?) # - wget vs. certificates? switch to curl? # - remove unneeded subshells? ( '()' -> '{}' ) # - slowly getting worse and worse, needs some refreshment and unification # -> the only exchange encoding is PEM # - can be combined with DER in one stream thanks to sed ranges filtering # whereas corresponding PEM can be appended after this DER so we # we can be sure the stream contains, perhaps interleaved, # an expected PEM instance # -> through away the trust in CA_BUNDLE completely? # -> better decomposition/DRY # - awk invocations, etc. # -> nicer "functional approach" (more pipes, less files [persisted state]) # - flock can be handy (see the other project of mine) # -> wider goals: mapping of certain cert chains (download locations, etc.) # -> migrate to other language? (but why) # # related references: # - http://curl.haxx.se/docs/caextract.html # - p7s: https://lists.fedoraproject.org/pipermail/devel/2013-February/178272.html : ${HOMEBUNDLE:=} [ -z ${HOMEBUNDLE} ] && source cert-def CA_BUNDLE=/etc/pki/tls/certs/ca-bundle.crt #WGET="wget -nv -U '' --ca-certificate <(cat "${CA_BUNDLE}" "${HOMEBUNDLE}")" WGET="wget -nv -U ''" guess_inform() { case "{1##*.}" in der) echo DER;; crt|pem|*) echo PEM;; esac } guess_vercmd() { echo "$1" | grep -q 'X509 CRL' && echo 'crl' || echo 'verify' } vercmd2cmd() { echo "$1" | grep -q 'crl' && echo 'crl' || echo 'x509' } guess_cmd() { case "$(basename "$1")" in crl*|revoke*|*crl) echo "crl";; *crt|*) echo "x509";; esac } # export because it is used in a separate bash invocation export -f guess_cmd cert_pick_file() { [[ "$1" =~ .*://.* ]] && return 1 echo "Trying file" >&2 local inform=$(guess_inform "$1") local cmd=$(guess_cmd "$1") [ -f "$1" ] && openssl ${cmd} -inform "${inform}" -in "$1" } # when CA cert(s) hosted on https server signed by this very CA # in case of cert chain, list them from root # TODO: check that the machine remains the same cert_pick_url_selfsigned() { local outtmpfile=$(mktemp /tmp/.XXXXXX) i=1 for c in $*; do [[ "$c" =~ https://.* ]] || return 1 echo "Trying self-signed $c $i" >&2 local ret= local start=${c##https://} local host=${start%%/*} local machine=${host%%:*} local port=${host#*:} [ "${port}" = "${machine}" ] && port=443 local cont=${start#*/} local inform=$(guess_inform "${cont}") [ "$(guess_cmd "${cont}")" = "x509" ] || return $? { echo -e "GET /${cont} HTTP/1.0\nHost: ${machine}\n"; sleep 2; } \ | openssl s_client -connect "${machine}:${port}" -crlf 2>/dev/null \ | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/{/-END CERTIFICATE-/{s|^\(-\+[A-Z ]\+-\+\).*|\1|};s|\r||g;p;d}' \ -e '/^\r$/,/-BEGIN CERTIFICATE-/{/-BEGIN CERTIFICATE-/p;d}' \ | { local tmpfile=$(mktemp /tmp/.XXXXXX) cat >${tmpfile} # try converting DER to PEM and appending to the original file awk '/-BEGIN CERTIFICATE-/{++i;}{if(i > 1){print;}}' ${tmpfile} \ | openssl x509 -inform DER -in /dev/stdin >${tmpfile}.1 cat ${tmpfile}.1 >> ${tmpfile} rm ${tmpfile}.1 [ $i -eq 1 ] \ || openssl verify -CAfile \ <(cat "${HOMEBUNDLE}" "${outtmpfile}") \ <(awk '/-BEGIN CERTIFICATE-/{++i;}{if(i > 1){print;}}' ${tmpfile}) >&2 ret=$? echo "i: $i, ret: $ret; $outtmpfile, $tmpfile" >&2 [ $ret -eq 0 ] \ && openssl x509 -inform "${inform}" -in \ <(awk '/-BEGIN CERTIFICATE-/{++i;}{if(i > 1){print;}}' ${tmpfile}) >>${outtmpfile} [ $i -eq $# ] \ && { awk '/-END CERTIFICATE-/{print; exit;}{print;}' ${tmpfile} \ | openssl verify -CAfile \ <(cat "${HOMEBUNDLE}" "${outtmpfile}") \ /dev/stdin >&2 || { ret=$?; rm -- ${outtmpfile} ${tmpfile}; return $ret; } } rm -- ${tmpfile} [ $ret -ne 0 ] && break; } let i+=1 done cat ${outtmpfile} rm -- ${outtmpfile} ||: } cert_pick_url() { echo "Trying URL.." >&2 local inform=$(guess_inform "$1") local cmd=$(guess_cmd "$1") (if ! ${WGET} "$1" -O- && [[ "$1" =~ https://.* ]]; then local start=${1##https://} local host=${start%%/*} local machine=${host%%:*} local port=${host#*:} [ "${port}" = "${machine}" ] && port=443 ( echo ">>> recursion" >&2 cert_pick_check "${machine}" "${port}" \ || cert_pick_check -nocrl "${machine}" "${port}" echo "<<< recursion" >&2 ) >&2 \ && ${WGET} --no-check-certificate "$1" -O- fi) | openssl ${cmd} -inform "${inform}" } cert_pick_from_server() { echo "Trying from server.." >&2 local server=$1 local port=443 # https local opts= [ $# -ge 2 ] && [[ "${2}" =~ ^[^-].* ]] && shift && port=$1 [ $# -ge 2 ] && opts=$2 ( echo; sleep 2 ) \ | openssl s_client -connect "${server}:${port}" -crlf $opts 2>/dev/null \ | sed -ne '/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/p' \ | awk '/-BEGIN CERTIFICATE-/{if(++i > 1){exit;}}{print;}' } cert_pick() { cert_pick_file "$@" \ || cert_pick_url_selfsigned "$@" \ || cert_pick_url "$@" \ || cert_pick_from_server "$@" } cert_check() { local ret= tmpfile=$(mktemp /tmp/.XXXXXX) cat >${tmpfile} export vercmd=$(local fst; read fst < <(head -n1 "${tmpfile}"); guess_vercmd "${fst}") openssl ${vercmd} $([ "$1" != "0" ] && echo '-crl_check') -CAfile \ <( openssl $(vercmd2cmd "${vercmd}") -noout -text -in ${tmpfile} \ | sed -n 's|.*URI:\(.\+\)|\1|p' \ | xargs -I '{}' bash -c "case \$(guess_cmd '{}') in \ verify) ${WGET} -O- '{}' | openssl x509 -outform PEM;; \ crl) ${WGET} -O- '{}' | openssl crl -outform PEM;; \ *) echo 'Sorry, URI {} not supported' >&2;; \ esac" \ | cat "${HOMEBUNDLE}" - 2>/dev/null ) \ $(echo "${vercmd}" | grep -q crl && echo '-in') ${tmpfile} >&2 ret=$? [ $ret -eq 0 ] && cat ${tmpfile} unset vercmd rm -- ${tmpfile} echo "$ret" >&2 return $ret } colorize() { # last line = exitcode ( ( test -t 1 || [ $# -ge 1 ] ) \ && sed -u \ -e 's|\(^Trying.*$\)|\x1b[33m\1\x1b[0m|' \ -e 's|\(^Adding.*$\)|\x1b[33m\1\x1b[0m|' \ -e 's|\(^error\s\+.*\)|\x1b[31m\1\x1b[0m|' \ -e 's|\(^\S\+:.*\)|\x1b[32m\1\x1b[0m|' \ -e 's|\(^\S\+\s\+\S\+\s\+URL:.*\)|\x1b[36m\1\x1b[0m|' \ || cat ) | awk 'FNR == 1 { last=$1; while (getline) { print last; last=$0; } exit last}' } cert_pick_check() { local crl=1 [ "$1" = "-nocrl" ] && shift && crl=0 cert_pick "$@" | cert_check $crl } setup() { set -u RESTOREUMASK=$(umask -p) umask 077 } teardown() { ${RESTOREUMASK} unset RESTOREUMASK unset vercmd return $1 } [[ "${BASH_SOURCE[0]}" != "${0}" ]] || \ { [ $# -lt 1 ] \ && echo "usage: $0" \ "[-nocrl] file-or-url-or-server [server-port=443]" \ || { setup; { cert_pick_check "$@"; echo $?; } |& colorize 1; teardown $?; } }