diff --git a/check_ssl_cert/check_ssl_cert b/check_ssl_cert/check_ssl_cert index 87fce2f..c0365fd 100644 --- a/check_ssl_cert/check_ssl_cert +++ b/check_ssl_cert/check_ssl_cert @@ -19,7 +19,7 @@ ################################################################################ # Constants -VERSION=1.31.0 +VERSION=1.37.0 SHORTNAME="SSL_CERT" VALID_ATTRIBUTES=",startdate,enddate,subject,issuer,serial,modulus,serial,hash,email,ocsp_uri,fingerprint," @@ -57,7 +57,9 @@ usage() { echo " -d,--debug produces debugging output" echo " -e,--email address pattern to match the email address contained in the" echo " certificate" + echo " --ecdsa cipher selection: force ECDSA authentication" echo " -f,--file file local file path (works with -H localhost only)" + echo " --file-bin path path of the file binary to be used" echo " -h,--help,-? this help message" echo " --ignore-exp ignore expiration date" echo " --ignore-sig-alg do not check if the certificate was signed with SHA1" @@ -73,7 +75,8 @@ usage() { echo " enddate, startdate, subject, issuer, modulus," echo " serial, hash, email, ocsp_uri and fingerprint." echo " 'all' will include all the available attributes." - echo " -n,--cn name pattern to match the CN of the certificate" + echo " -n,--cn name pattern to match the CN of the certificate (can be" + echo " specified multiple times)" echo " --no_ssl2 disable SSL version 2" echo " --no_ssl3 disable SSL version 3" echo " --no_tls1 disable TLS version 1" @@ -92,6 +95,7 @@ usage() { echo " --ssl3 force SSL version 3" echo " -r,--rootcert path root certificate or directory to be used for" echo " certificate validation" + echo " --rsa cipher selection: force RSA authentication" echo " -t,--timeout seconds timeout after the specified time" echo " (defaults to 15 seconds)" echo " --temp dir directory where to store the temporary files" @@ -110,7 +114,7 @@ usage() { echo " -S,--ssl version force SSL version (2,3)" echo " (see: --ss2 or --ssl3)" echo - echo "Report bugs to: Matteo Corti " + echo "Report bugs to https://github.com/matteocorti/check_ssl_cert/issues" echo exit 3 @@ -287,18 +291,23 @@ convert_ssl_lab_grade() { fetch_certificate() { + RET=0 + # Check if a protocol was specified (if not HTTP switch to TLS) if [ -n "${PROTOCOL}" ] && [ "${PROTOCOL}" != "http" ] && [ "${PROTOCOL}" != "https" ] ; then case "${PROTOCOL}" in smtp) - exec_with_timeout "$TIMEOUT" "echo -e 'QUIT\r' | $OPENSSL s_client ${CLIENT} ${CLIENTPASS} -starttls ${PROTOCOL} -connect $HOST:$PORT ${SERVERNAME} -verify 6 ${ROOT_CA} ${SSL_VERSION} ${SSL_VERSION_DISABLED} 2> ${ERROR} 1> ${CERT}" + exec_with_timeout "$TIMEOUT" "echo -e 'QUIT\r' | $OPENSSL s_client ${CLIENT} ${CLIENTPASS} -starttls ${PROTOCOL} -connect $HOST:$PORT ${SERVERNAME} -verify 6 ${ROOT_CA} ${SSL_VERSION} ${SSL_VERSION_DISABLED} ${SSL_AU} 2> ${ERROR} 1> ${CERT}" + RET=$? ;; irc) - exec_with_timeout "$TIMEOUT" "echo -e 'QUIT\r' | $OPENSSL s_client ${CLIENT} ${CLIENTPASS} -connect $HOST:$PORT ${SERVERNAME} -verify 6 ${ROOT_CA} ${SSL_VERSION} ${SSL_VERSION_DISABLED} 2> ${ERROR} 1> ${CERT}" + exec_with_timeout "$TIMEOUT" "echo -e 'QUIT\r' | $OPENSSL s_client ${CLIENT} ${CLIENTPASS} -connect $HOST:$PORT ${SERVERNAME} -verify 6 ${ROOT_CA} ${SSL_VERSION} ${SSL_VERSION_DISABLED} ${SSL_AU} 2> ${ERROR} 1> ${CERT}" + RET=$? ;; pop3|imap|ftp|xmpp) - exec_with_timeout "$TIMEOUT" "echo 'Q' | $OPENSSL s_client ${CLIENT} ${CLIENTPASS} -starttls ${PROTOCOL} -connect $HOST:$PORT ${SERVERNAME} -verify 6 ${ROOT_CA} ${SSL_VERSION} ${SSL_VERSION_DISABLED} 2> ${ERROR} 1> ${CERT}" + exec_with_timeout "$TIMEOUT" "echo 'Q' | $OPENSSL s_client ${CLIENT} ${CLIENTPASS} -starttls ${PROTOCOL} -connect $HOST:$PORT ${SERVERNAME} -verify 6 ${ROOT_CA} ${SSL_VERSION} ${SSL_VERSION_DISABLED} ${SSL_AU} 2> ${ERROR} 1> ${CERT}" + RET=$? ;; *) unknown "Error: unsupported protocol ${PROTOCOL}" @@ -309,13 +318,15 @@ fetch_certificate() { if [ "${HOST}" = "localhost" ] ; then exec_with_timeout "$TIMEOUT" "/bin/cat '${FILE}' 2> ${ERROR} 1> ${CERT}" + RET=$? else unknown "Error: option 'file' works with -H localhost only" fi else - exec_with_timeout "$TIMEOUT" "echo 'Q' | $OPENSSL s_client ${CLIENT} ${CLIENTPASS} -connect $HOST:$PORT ${SERVERNAME} -verify 6 ${ROOT_CA} ${SSL_VERSION} ${SSL_VERSION_DISABLED} 2> ${ERROR} 1> ${CERT}" + exec_with_timeout "$TIMEOUT" "echo 'Q' | $OPENSSL s_client ${CLIENT} ${CLIENTPASS} -connect $HOST:$PORT ${SERVERNAME} -verify 6 ${ROOT_CA} ${SSL_VERSION} ${SSL_VERSION_DISABLED} ${SSL_AU} 2> ${ERROR} 1> ${CERT}" + RET=$? fi @@ -326,7 +337,7 @@ fetch_certificate() { cp "${ERROR}" "${HOST}.error" fi - if [ $? -ne 0 ] ; then + if [ "${RET}" -ne 0 ] ; then if [ -n "${DEBUG}" ] ; then sed 's/^/[DBG] SSL error: /' "${ERROR}" @@ -357,6 +368,18 @@ fetch_certificate() { } +################################################################################ +# Adds metric to performance data +# Params +# $1 performance data in nagios plugin format, +# see https://nagios-plugins.org/doc/guidelines.html#AEN200 +add_performance_data() { + if [ -z "${PERFORMANCE_DATA}" ]; then + PERFORMANCE_DATA="|${1}" + else + PERFORMANCE_DATA="${PERFORMANCE_DATA} $1" + fi +} ################################################################################ # Main @@ -366,6 +389,7 @@ main() { # Default values DEBUG="" OPENSSL="" + FILE_BIN="" IGNORE_SSL_LABS_CACHE="" PORT="443" TIMEOUT="15" @@ -444,6 +468,14 @@ main() { SELFSIGNED=1 shift ;; + --rsa) + SSL_AU="-cipher aRSA" + shift + ;; + --ecdsa) + SSL_AU="-cipher aECDSA" + shift + ;; --ssl2) SSL_VERSION="-ssl2" shift @@ -515,6 +547,14 @@ main() { unknown "-f,--file requires an argument" fi ;; + --file-bin) + if [ $# -gt 1 ]; then + FILE_BIN="$2" + shift 2 + else + unknown "--file-bin requires an argument" + fi + ;; -H|--host) if [ $# -gt 1 ]; then HOST="$2" @@ -557,7 +597,11 @@ main() { ;; -n|--cn) if [ $# -gt 1 ]; then - COMMON_NAME="$2" + if [ -n "${COMMON_NAME}" ]; then + COMMON_NAME="${COMMON_NAME} ${2}" + else + COMMON_NAME="${2}" + fi shift 2 else unknown "-n,--cn requires an argument" @@ -785,6 +829,12 @@ main() { OPENSSL=$PROG fi + # file + if [ -z "${FILE_BIN}" ] ; then + check_required_prog file + FILE_BIN=$PROG + fi + # Expect (optional) EXPECT="$(which expect 2> /dev/null)" test -x "${EXPECT}" || EXPECT="" @@ -813,25 +863,57 @@ main() { echo "disabling timeouts" fi - # Perl with Date::Parse (optional) PERL="$(which perl 2> /dev/null)" - test -x "${PERL}" || PERL="" - if [ -z "${PERL}" ] && [ -n "${VERBOSE}" ] ; then - echo "Perl not found: disabling date computations" + + if [ -n "${DEBUG}" ] && [ -n "${PERL}" ] ; then + echo "[DBG] perl available: ${PERL}" + fi + + DATEBIN="$(which date 2> /dev/null)" + + if [ -n "${DEBUG}" ] && [ -n "${DATEBIN}" ] ; then + echo "[DBG] date available: ${DATEBIN}" fi - if ! ${PERL} -e "use Date::Parse;" > /dev/null 2>&1 ; then + DATETYPE="" + + if ! "${DATEBIN}" +%s >/dev/null 2>&1 ; then + + # Perl with Date::Parse (optional) + test -x "${PERL}" || PERL="" + if [ -z "${PERL}" ] && [ -n "${VERBOSE}" ] ; then + echo "Perl not found: disabling date computations" + fi + + if ! ${PERL} -e "use Date::Parse;" > /dev/null 2>&1 ; then + + if [ -n "${VERBOSE}" ] ; then + echo "Perl module Date::Parse not installed: disabling date computations" + fi + + PERL="" + + else + + if [ -n "${VERBOSE}" ] ; then + echo "Perl module Date::Parse installed: enabling date computations" + fi + + DATETYPE="PERL" - if [ -n "${VERBOSE}" ] ; then - echo "Perl module Date::Parse not installed: disabling date computations" fi - PERL="" else - if [ -n "${VERBOSE}" ] ; then - echo "Perl module Date::Parse installed: enabling date computations" - fi + if $DATEBIN --version >/dev/null 2>&1 ; then + DATETYPE="GNU" + else + DATETYPE="BSD" + fi + + if [ -n "${VERBOSE}" ] ; then + echo "found ${DATETYPE} date with timestamp support: enabling date computations" + fi fi @@ -851,13 +933,18 @@ main() { # SERVERNAME= if ${OPENSSL} s_client not_a_real_option 2>&1 | grep -q -- -servername ; then - - if [ -n "${COMMON_NAME}" ] ; then + + if [ -n "${COMMON_NAME}" ] && [ "${COMMON_NAME}" = "$(echo "${COMMON_NAME}" | tr -d ' ')" ] ; then SERVERNAME="-servername ${COMMON_NAME}" else SERVERNAME="-servername ${HOST}" fi + if [ -n "${DEBUG}" ] ; then + echo "[DBG] '${OPENSSL} s_client' supports '-servername': using ${SERVERNAME}" + fi + + else if [ -n "${VERBOSE}" ] ; then @@ -957,7 +1044,7 @@ main() { fi if [ -n "${VERBOSE}" ] ; then - echo "Parsing the certificate file" + echo "parsing the certificate file" fi ################################################################################ @@ -977,6 +1064,13 @@ main() { ISSUER_URI="$($OPENSSL x509 -in "${CERT}" -text -noout | grep "CA Issuers" | sed -e "s/^.*CA Issuers - URI://")" + if [ -z "${ISSUER_URI}" ] ; then + if [ -n "${VERBOSE}" ] ; then + echo "cannot find the CA Issuers in the certificate: disabling OCSP checks" + fi + OCSP="" + fi + SIGNATURE_ALGORITHM="$($OPENSSL x509 -in "${CERT}" -text -noout | grep 'Signature Algorithm' | head -n 1)" if [ -n "${DEBUG}" ] ; then @@ -1050,23 +1144,40 @@ main() { ################################################################################ # Compute for how many days the certificate will be valid - if [ -n "${PERL}" ] ; then + if [ -n "${DATETYPE}" ]; then CERT_END_DATE=$($OPENSSL x509 -in "${CERT}" -noout -enddate | sed -e "s/.*=//") - DAYS_VALID=$(perl - "${CERT_END_DATE}" <<-"EOF" - use strict; - use warnings; + OLDLANG=$LANG + LANG=en_US - use Date::Parse; + if [ -n "${DEBUG}" ] ; then + echo "[DBG] Date computations: ${DATETYPE}" + fi - my $cert_date = str2time( $ARGV[0] ); + case "${DATETYPE}" in + "BSD") + DAYS_VALID=$(( ( $(${DATEBIN} -jf "%b %d %T %Y %Z" "${CERT_END_DATE}" +%s) - $(${DATEBIN} +%s) ) / 86400 )) + ;; - my $days = int (( $cert_date - time ) / 86400 + 0.5); + "GNU") + DAYS_VALID=$(( ( $(${DATEBIN} -d "${CERT_END_DATE}" +%s) - $(${DATEBIN} +%s) ) / 86400 )) + ;; - print "$days\n"; - EOF - ) + "PERL") + DAYS_VALID=$(perl - "${CERT_END_DATE}" <<-"EOF" + use strict; + use warnings; + use Date::Parse; + my $cert_date = str2time( $ARGV[0] ); + my $days = int (( $cert_date - time ) / 86400 + 0.5); + print "$days\n"; + EOF + ) + ;; + esac + + LANG=$OLDLANG if [ -n "${VERBOSE}" ] ; then @@ -1077,75 +1188,105 @@ main() { fi fi - PERFORMANCE_DATA="|days=$DAYS_VALID;${WARNING};${CRITICAL};;" + add_performance_data "days=$DAYS_VALID;${WARNING};${CRITICAL};;" fi ################################################################################ # Check the CN if [ -n "$COMMON_NAME" ] ; then - + ok="" if [ -n "${DEBUG}" ] ; then echo "[DBG] check CN: ${CN}" fi - if echo "${CN}" | grep -q "^\*\." ; then + # Common name is case insensitive: using grep for comparison (and not 'case' with 'shopt -s nocasematch' as not defined in POSIX + + if echo "${CN}" | grep -q -i "^\*\." ; then # Match the domain - if echo "${COMMON_NAME}" | grep -q "^$(echo "${CN}" | cut -c 3-)\$" ; then - + if [ -n "${DEBUG}" ] ; then + echo "[DBG] the common name ${CN} begins with a '*'" + echo "[DBG] checking if the common name matches ^$(echo "${CN}" | cut -c 3-)\$" + fi + if echo "${COMMON_NAME}" | grep -q -i "^$(echo "${CN}" | cut -c 3-)\$" ; then if [ -n "${DEBUG}" ] ; then echo "[DBG] the common name ${COMMON_NAME} matches ^$( echo "${CN}" | cut -c 3- )\$" fi ok="true" + fi # Or the literal with the wildcard - if echo "${COMMON_NAME}" | grep -q "^$(echo "${CN}" | sed -e 's/[.]/[.]/g' -e 's/[*]/[A-Za-z0-9\-]*/' )\$" ; then - + if [ -n "${DEBUG}" ] ; then + echo "[DBG] checking if the common name matches ^$(echo "${CN}" | sed -e 's/[.]/[.]/g' -e 's/[*]/[A-Za-z0-9\-]*/' )\$" + fi + if echo "${COMMON_NAME}" | grep -q -i "^$(echo "${CN}" | sed -e 's/[.]/[.]/g' -e 's/[*]/[A-Za-z0-9\-]*/' )\$" ; then if [ -n "${DEBUG}" ] ; then echo "[DBG] the common name ${COMMON_NAME} matches ^$(echo "${CN}" | sed -e 's/[.]/[.]/g' -e 's/[*]/[A-Za-z0-9\-]*/' )\$" fi ok="true" fi + # Or if both are exactly the same + if [ -n "${DEBUG}" ] ; then + echo "[DBG] checking if the common name matches ^${CN}\$" + fi + if echo "${COMMON_NAME}" | grep -q -i "^${CN}\$" ; then + if [ -n "${DEBUG}" ] ; then + echo "[DBG] the common name ${COMMON_NAME} matches ^${CN}\$" + fi + ok="true" + fi + else - case "${COMMON_NAME}" in - ${CN}) - ok="true" - ;; - esac - + if echo "${COMMON_NAME}" | grep -q -i "^${CN}$" ; then + ok="true" + fi + fi # Check alterante names if [ -n "${ALTNAMES}" ] ; then - if [ -n "${DEBUG}" ] ; then - echo "[DBG] checking altnames" - fi + for cn in ${COMMON_NAME} ; do - for alt_name in $($OPENSSL x509 -in "${CERT}" -text \ - | grep --after-context=1 "509v3 Subject Alternative Name:" \ - | tail -n 1 | sed -e "s/DNS://g" | sed -e "s/,//g") ; do + ok="" if [ -n "${DEBUG}" ] ; then - echo "[DBG] ${alt_name}" + echo "[DBG] checking altnames against ${cn}" fi - case "$COMMON_NAME" in - "$alt_name") + for alt_name in $($OPENSSL x509 -in "${CERT}" -text \ + | grep --after-context=1 "509v3 Subject Alternative Name:" \ + | tail -n 1 | sed -e "s/DNS://g" | sed -e "s/,//g") ; do + + if [ -n "${DEBUG}" ] ; then + echo "[DBG] ${alt_name}" + fi + + if echo "${cn}" | grep -q -i "^${alt_name}$" ; then ok="true" - ;; - esac + fi + + done + + if [ -z "$ok" ] ; then + fail=$cn + break; + fi done fi + if [ -n "$fail" ] ; then + critical "invalid CN ('$CN' does not match '$fail')" + fi + if [ -z "$ok" ] ; then critical "invalid CN ('$CN' does not match '$COMMON_NAME')" fi @@ -1308,6 +1449,8 @@ main() { convert_ssl_lab_grade "${SSL_LABS_HOST_GRADE}" SSL_LABS_HOST_GRADE_NUMERIC="${NUMERIC_SSL_LAB_GRADE}" + add_performance_data "ssllabs=${SSL_LABS_HOST_GRADE_NUMERIC}%;;${SSL_LAB_ASSESTMENT_NUMERIC}" + # Check the grade if [ "${SSL_LABS_HOST_GRADE_NUMERIC}" -lt "${SSL_LAB_ASSESTMENT_NUMERIC}" ] ; then critical "SSL Labs grade is ${SSL_LABS_HOST_GRADE} (instead of ${SSL_LAB_ASSESTMENT})" @@ -1365,19 +1508,42 @@ main() { fi curl --silent "${ISSUER_URI}" > "${ISSUER_CERT}" - - if file "${ISSUER_CERT}" | grep -q ': data' ; then - if [ -n "${DEBUG}" ] ; then - echo "[DBG] OCSP: converting issuer certificate from DER to PEM" + if [ -n "${DEBUG}" ] ; then + echo "[DBG] OCSP: issuer certificate type: $(${FILE_BIN} "${ISSUER_CERT}" | sed 's/.*://' )" + fi + + # check the result + if ! "${FILE_BIN}" "${ISSUER_CERT}" | grep -q ': (ASCII|PEM)' ; then + + if "${FILE_BIN}" "${ISSUER_CERT}" | grep -q ': data' ; then + + if [ -n "${DEBUG}" ] ; then + echo "[DBG] OCSP: converting issuer certificate from DER to PEM" + fi + + openssl x509 -inform DER -outform PEM -in "${ISSUER_CERT}" -out "${ISSUER_CERT}" + + else + + unknown "Unable to fetch OCSP issuer certificate." + fi - - openssl x509 -inform DER -outform PEM -in "${ISSUER_CERT}" -out "${ISSUER_CERT}" + + fi if [ -n "${DEBUG}" ] ; then - echo "[DBG] OCSP: storing a copy of the retrieved issuer certificate to ${ISSUER_URI##*/}" - cp "${ISSUER_CERT}" "${ISSUER_URI##*/}" + + # remove trailing / + FILE_NAME=${ISSUER_URI%/} + + # remove everything up to the last slash + FILE_NAME=${FILE_NAME##*/} + + echo "[DBG] OCSP: storing a copy of the retrieved issuer certificate to ${FILE_NAME}" + + cp "${ISSUER_CERT}" "${FILE_NAME}" fi OCSP_HOST="$(echo "${OCSP_URI}" | sed -e "s@.*//\([^/]\+\)\(/.*\)\?\$@\1@g" | sed 's/^http:\/\///' | sed 's/\/.*//' )" @@ -1388,17 +1554,39 @@ main() { # check if -header is supported OCSP_HEADER="" - if "${OPENSSL}" ocsp 2>&1 | grep -q -- -header ; then + + # oscp -header is supported in OpenSSL versions from 1.0.0, but not documented until 1.1.0 + # so we check if the major version is greater than 0 + if [ "$( ${OPENSSL} version | sed -e 's/OpenSSL \([0-9]\).*/\1/g' )" -gt 0 ]; then if [ -n "${DEBUG}" ] ; then echo "[DBG] openssl ocsp support the -header option" fi - if [ -n "${DEBUG}" ] ; then - echo "[DBG] executing $OPENSSL ocsp -no_nonce -issuer ${ISSUER_CERT} -cert ${CERT} -url ${OCSP_URI} ${OCSP_HEADER} -header HOST ${OCSP_HOST}" + # http_proxy is sometimes lower- and sometimes uppercase. Programs usually check both + # shellcheck disable=SC2154 + if [ -n "${http_proxy}" ] ; then + HTTP_PROXY="${http_proxy}" fi - - OCSP_RESP="$($OPENSSL ocsp -no_nonce -issuer "${ISSUER_CERT}" -cert "${CERT}" -url "${OCSP_URI}" -header HOST "${OCSP_HOST}" 2>&1 | grep -i "ssl_cert")" + + if [ -n "${HTTP_PROXY:-}" ] ; then + + if [ -n "${DEBUG}" ] ; then + echo "[DBG] executing $OPENSSL ocsp -no_nonce -issuer ${ISSUER_CERT} -cert ${CERT} -host ${HTTP_PROXY#*://} -path ${OCSP_URI} -header HOST ${OCSP_HOST}" + fi + + OCSP_RESP="$($OPENSSL ocsp -no_nonce -issuer "${ISSUER_CERT}" -cert "${CERT}" -host "${HTTP_PROXY#*://}" -path "${OCSP_URI}" -header HOST "${OCSP_HOST}" 2>&1 | grep -i "ssl_cert")" + + else + + if [ -n "${DEBUG}" ] ; then + echo "[DBG] executing $OPENSSL ocsp -no_nonce -issuer ${ISSUER_CERT} -cert ${CERT} -url ${OCSP_URI} ${OCSP_HEADER} -header HOST ${OCSP_HOST}" + fi + + OCSP_RESP="$($OPENSSL ocsp -no_nonce -issuer "${ISSUER_CERT}" -cert "${CERT}" -url "${OCSP_URI}" -header HOST "${OCSP_HOST}" 2>&1 | grep -i "ssl_cert")" + + + fi if [ -n "${DEBUG}" ] ; then echo "[DBG] OCSP: response = ${OCSP_RESP}" @@ -1408,7 +1596,11 @@ main() { critical "certificate is revoked" elif ! echo "${OCSP_RESP}" | grep -qi "good" ; then - OCSP_RESP="$($OPENSSL ocsp -no_nonce -issuer "${ISSUER_CERT}" -cert "${CERT}" -url "${OCSP_URI}" "${OCSP_HEADER}" 2>&1 )" + if [ -n "${HTTP_PROXY:-}" ] ; then + OCSP_RESP="$($OPENSSL ocsp -no_nonce -issuer "${ISSUER_CERT}" -cert "${CERT}" -host "${HTTP_PROXY#*://}" -path "${OCSP_URI}" "${OCSP_HEADER}" 2>&1 )" + else + OCSP_RESP="$($OPENSSL ocsp -no_nonce -issuer "${ISSUER_CERT}" -cert "${CERT}" -url "${OCSP_URI}" "${OCSP_HEADER}" 2>&1 )" + fi critical "${OCSP_RESP}" fi diff --git a/check_ssl_cert/control b/check_ssl_cert/control index cae2971..ac88e59 100644 --- a/check_ssl_cert/control +++ b/check_ssl_cert/control @@ -1,7 +1,7 @@ Homepage: https://github.com/matteocorti/check_ssl_cert/blob/master/check_ssl_cert Watch: https://raw.githubusercontent.com/matteocorti/check_ssl_cert/master/check_ssl_cert VERSION=([0-9.]+) Recommends: ca-certificates, expect, libtimedate-perl, openssl -Version: 1.31.0 +Version: 1.37.0 Uploaders: Jan Wagner Description: plugin checking an X.509 certificate - checks if the server is running and delivers a valid certificate