#!/bin/sh # # check_ssl_cert # # Checks an X.509 certificate: # - checks if the server is running and delivers a valid certificate # - checks if the CA matches a given pattern # - checks the validity # # See the INSTALL file for installation instructions # # Copyright (c) 2007-2012 ETH Zurich. # Copyright (c) 2007-2016 Matteo Corti # # This module is free software; you can redistribute it and/or modify it # under the terms of GNU general public license (gpl) version 3. # See the LICENSE file for details. ################################################################################ # Constants VERSION=1.37.0 SHORTNAME="SSL_CERT" VALID_ATTRIBUTES=",startdate,enddate,subject,issuer,serial,modulus,serial,hash,email,ocsp_uri,fingerprint," ################################################################################ # Functions ################################################################################ # Prints usage information # Params # $1 error message (optional) usage() { if [ -n "$1" ] ; then echo "Error: $1" 1>&2 fi #### The following line is 80 characters long (helps to fit the help text in a standard terminal) ######-------------------------------------------------------------------------------- echo echo "Usage: check_ssl_cert -H host [OPTIONS]" echo echo "Arguments:" echo " -H,--host host server" echo echo "Options:" echo " -A,--noauth ignore authority warnings (expiration only)" echo " --altnames matches the pattern specified in -n with alternate" echo " names too" echo " -C,--clientcert path use client certificate to authenticate" echo " --clientpass phrase set passphrase for client certificate." echo " -c,--critical days minimum number of days a certificate has to be valid" echo " to issue a critical status" 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" echo " or MD5" echo " --ignore-ocsp do not check revocation with OCSP" echo " -i,--issuer issuer pattern to match the issuer of the certificate" echo " -L,--check-ssl-labs grade SSL Labs assestment" echo " (please check https://www.ssllabs.com/about/terms.html)" echo " --ignore-ssl-labs-cache Forces a new check by SSL Labs (see -L)" echo " --long-output list append the specified comma separated (no spaces) list" echo " of attributes to the plugin output on additional lines" echo " Valid attributes are:" 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 (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" echo " --no_tls1_1 disable TLS version 1.1" echo " --no_tls1_2 disable TLS version 1.2" echo " -N,--host-cn match CN with the host name" echo " -o,--org org pattern to match the organization of the certificate" echo " --openssl path path of the openssl binary to be used" echo " -p,--port port TCP port" echo " -P,--protocol protocol use the specific protocol {http|smtp|pop3|imap|ftp|xmpp|irc}" echo " http: default" echo " smtp,pop3,imap,ftp: switch to TLS" echo " -s,--selfsigned allows self-signed certificates" echo " --serial serialnum pattern to match the serial number" echo " --ssl2 force SSL version 2" 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" echo " --tls1 force TLS version 1" echo " --tls1_1 force TLS version 1.1" echo " --tls1_2 force TLS version 1.2" echo " -v,--verbose verbose output" echo " -V,--version version" echo " -w,--warning days minimum number of days a certificate has to be valid" echo " to issue a warning status" echo echo "Deprecated options:" echo " -d,--days days minimum number of days a certificate has to be valid" echo " (see --critical and --warning)" echo " --ocsp check revocation via OCSP" echo " -S,--ssl version force SSL version (2,3)" echo " (see: --ss2 or --ssl3)" echo echo "Report bugs to https://github.com/matteocorti/check_ssl_cert/issues" echo exit 3 } ################################################################################ # Exits with a critical message # Params # $1 error message critical() { if [ -n "${CN}" ] ; then tmp=" ${CN}" fi printf '%s CRITICAL%s: %s%s%s\n' "${SHORTNAME}" "${tmp}" "$1" "${PERFORMANCE_DATA}" "${LONG_OUTPUT}" exit 2 } ################################################################################ # Exits with a warning message # Param # $1 warning message warning() { if [ -n "${CN}" ] ; then tmp=" ${CN}" fi printf '%s WARN%s: %s%s%s\n' "${SHORTNAME}" "${tmp}" "$1" "${PERFORMANCE_DATA}" "${LONG_OUTPUT}" exit 1 } ################################################################################ # Exits with an 'unkown' status # Param # $1 message unknown() { if [ -n "${CN}" ] ; then tmp=" ${CN}" fi printf '%s UNKNOWN%s: %s\n' "${SHORTNAME}" "${tmp}" "$1" exit 3 } ################################################################################ # Executes command with a timeout # Params: # $1 timeout in seconds # $2 command # Returns 1 if timed out 0 otherwise exec_with_timeout() { time=$1 # start the command in a subshell to avoid problem with pipes # (spawn accepts one command) command="/bin/sh -c \"$2\"" if [ -n "${DEBUG}" ] ; then echo "[DBG] executing with timeout (${time}s): $2" fi if [ -n "${TIMEOUT_BIN}" ] ; then if [ -n "${DEBUG}" ] ; then echo "[DBG] ${TIMEOUT_BIN} $time $command" fi eval "${TIMEOUT_BIN} $time $command" > /dev/null 2>&1 if [ $? = 137 ] ; then critical "Timeout after ${time} seconds" fi elif [ -n "${EXPECT}" ] ; then expect -c "set echo \"-noecho\"; set timeout $time; spawn -noecho $command; expect timeout { exit 1 } eof { exit 0 }" if [ $? = 1 ] ; then critical "Timeout after ${time} seconds" fi else eval "${command}" fi } ################################################################################ # Checks if a given program is available and executable # Params # $1 program name # Returns 1 if the program exists and is executable check_required_prog() { PROG=$(which "$1" 2> /dev/null) if [ -z "$PROG" ] ; then critical "cannot find $1" fi if [ ! -x "$PROG" ] ; then critical "$PROG is not executable" fi } ################################################################################ # Converts SSL Labs grades to a numeric value # (see https://www.ssllabs.com/downloads/SSL_Server_Rating_Guide.pdf) # Params # $1 program name # Sets NUMERIC_SSL_LAB_GRADE convert_ssl_lab_grade() { GRADE="$1" unset NUMERIC_SSL_LAB_GRADE case "${GRADE}" in 'A+'|'a+') # Value not in documentation NUMERIC_SSL_LAB_GRADE=85 shift ;; A|a) NUMERIC_SSL_LAB_GRADE=80 shift ;; 'A-'|'a-') # Value not in documentation NUMERIC_SSL_LAB_GRADE=75 shift ;; B|b) NUMERIC_SSL_LAB_GRADE=65 shift ;; C|c) NUMERIC_SSL_LAB_GRADE=50 shift ;; D|d) NUMERIC_SSL_LAB_GRADE=35 shift ;; E|e) NUMERIC_SSL_LAB_GRADE=20 shift ;; F|f) NUMERIC_SSL_LAB_GRADE=0 shift ;; T|t) # No trust: value not in documentation NUMERIC_SSL_LAB_GRADE=0 shift ;; M|m) # Certificate name mismatch: value not in documentation NUMERIC_SSL_LAB_GRADE=0 shift ;; *) unknown "Connot convert SSL Lab grade ${GRADE}" ;; esac } ################################################################################ # Tries to fetch the certificate 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} ${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} ${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} ${SSL_AU} 2> ${ERROR} 1> ${CERT}" RET=$? ;; *) unknown "Error: unsupported protocol ${PROTOCOL}" ;; esac elif [ -n "${FILE}" ] ; then 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} ${SSL_AU} 2> ${ERROR} 1> ${CERT}" RET=$? fi if [ -n "${DEBUG}" ] ; then echo "[DBG] storing a copy of the retrieved certificate in ${HOST}.crt" cp "${CERT}" "${HOST}.crt" echo "[DBG] storing a copy of the OpenSSL errors in ${HOST}.error" cp "${ERROR}" "${HOST}.error" fi if [ "${RET}" -ne 0 ] ; then if [ -n "${DEBUG}" ] ; then sed 's/^/[DBG] SSL error: /' "${ERROR}" fi # s_client could verify the server certificate because the server requires a client certificate if grep -q '^Acceptable client certificate CA names' "${CERT}" ; then if [ -n "${VERBOSE}" ] ; then echo "The server requires a client certificate" fi else # Try to clean up the error message # Remove the 'verify and depth' lines # Take the 1st line (seems OK with the use cases I tested) ERROR_MESSAGE=$( grep -v '^depth' "${ERROR}" \ | grep -v '^verify' \ | head -n 1 ) critical "SSL error: ${ERROR_MESSAGE}" fi fi } ################################################################################ # 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 ################################################################################ main() { # Default values DEBUG="" OPENSSL="" FILE_BIN="" IGNORE_SSL_LABS_CACHE="" PORT="443" TIMEOUT="15" VERBOSE="" OCSP="1" # enabled by default # Set the default temp dir if not set if [ -z "${TMPDIR}" ] ; then TMPDIR="/tmp" fi ################################################################################ # Process command line options # # We do no use getopts since it is unable to process long options while true; do case "$1" in ######################################## # Options without arguments -A|--noauth) NOAUTH=1 shift ;; --altnames) ALTNAMES=1 shift ;; -d|--debug) DEBUG=1 VERBOSE=1 shift ;; -h|--help|-\?) usage exit 0 ;; --ignore-exp) NOEXP=1 shift ;; --ignore-sig-alg) NOSIGALG=1 shift ;; --ignore-ssl-labs-cache) IGNORE_SSL_LABS_CACHE="&startNew=on" shift ;; --no_ssl2) SSL_VERSION_DISABLED="${SSL_VERSION_DISABLED} -no_ssl2" shift ;; --no_ssl3) SSL_VERSION_DISABLED="${SSL_VERSION_DISABLED} -no_ssl3" shift ;; --no_tls1) SSL_VERSION_DISABLED="${SSL_VERSION_DISABLED} -no_tls1" shift ;; --no_tls1_1) SSL_VERSION_DISABLED="${SSL_VERSION_DISABLED} -no_tls1_1" shift ;; --no_tls1_2) SSL_VERSION_DISABLED="${SSL_VERSION_DISABLED} -no_tls1_2" shift ;; -N|--host-cn) COMMON_NAME="__HOST__" shift ;; -s|--selfsigned) SELFSIGNED=1 shift ;; --rsa) SSL_AU="-cipher aRSA" shift ;; --ecdsa) SSL_AU="-cipher aECDSA" shift ;; --ssl2) SSL_VERSION="-ssl2" shift ;; --ssl3) SSL_VERSION="-ssl3" shift ;; --tls1) SSL_VERSION="-tls1" shift ;; --tls1_1) SSL_VERSION="-tls1_1" shift ;; --tls1_2) SSL_VERSION="-tls1_2" shift ;; --ocsp) # deprecated shift ;; --ignore-ocsp) OCSP="" shift ;; -v|--verbose) VERBOSE=1 shift ;; -V|--version) echo "check_ssl_cert version ${VERSION}" exit 3 ;; ######################################## # Options with arguments -c|--critical) if [ $# -gt 1 ]; then CRITICAL="$2" shift 2 else unknown "-c,--critical requires an argument" fi ;; # Deprecated option: used to be as --warning -d|--days) if [ $# -gt 1 ]; then WARNING="$2" shift 2 else unknown "-d,--days requires an argument" fi ;; -e|--email) if [ $# -gt 1 ]; then ADDR="$2" shift 2 else unknown "-e,--email requires an argument" fi ;; -f|--file) if [ $# -gt 1 ]; then FILE="$2" shift 2 else 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" shift 2 else unknown "-H,--host requires an argument" fi ;; -i|--issuer) if [ $# -gt 1 ]; then ISSUER="$2" shift 2 else unknown "-i,--issuer requires an argument"s fi ;; -L|--check-ssl-labs) if [ $# -gt 1 ]; then SSL_LAB_ASSESTMENT="$2" shift 2 else unknown "-L|--check-ssl-labs requires an argument" fi ;; --serial) if [ $# -gt 1 ]; then SERIAL_LOCK="$2" shift 2 else unknown "-i,--issuer requires an argument" fi ;; --long-output) if [ $# -gt 1 ]; then LONG_OUTPUT_ATTR="$2" shift 2 else unknown "--long-output requires an argument" fi ;; -n|--cn) if [ $# -gt 1 ]; then if [ -n "${COMMON_NAME}" ]; then COMMON_NAME="${COMMON_NAME} ${2}" else COMMON_NAME="${2}" fi shift 2 else unknown "-n,--cn requires an argument" fi ;; -o|--org) if [ $# -gt 1 ]; then ORGANIZATION="$2" shift 2 else unknown "-o,--org requires an argument" fi ;; --openssl) if [ $# -gt 1 ]; then OPENSSL="$2" shift 2 else unknown "--openssl requires an argument" fi ;; -p|--port) if [ $# -gt 1 ]; then PORT="$2" shift 2 else unknown "-p,--port requires an argument" fi ;; -P|--protocol) if [ $# -gt 1 ]; then PROTOCOL="$2" shift 2 else unknown "-P,--protocol requires an argument" fi ;; -r|--rootcert) if [ $# -gt 1 ]; then ROOT_CA="$2" shift 2 else unknown "-r,--rootcert requires an argument" fi ;; -C|--clientcert) if [ $# -gt 1 ]; then CLIENT_CERT="$2" shift 2 else unknown "-c,--clientcert requires an argument" fi ;; --clientpass) if [ $# -gt 1 ]; then CLIENT_PASS="$2" shift 2 else unknown "--clientpass requires an argument" fi ;; -S|--ssl) if [ $# -gt 1 ]; then if [ "$2" = "2" ] || [ "$2" = "3" ] ; then SSL_VERSION="-ssl${2}" shift 2 else unknown "invalid argument for --ssl" fi else unknown "--ssl requires an argument" fi ;; -t|--timeout) if [ $# -gt 1 ]; then TIMEOUT="$2" shift 2 else unknown "-t,--timeout requires an argument" fi ;; --temp) if [ $# -gt 1 ] ; then # Override TMPDIR TMPDIR="$2" shift 2 else unknown "--temp requires an argument" fi ;; -w|--warning) if [ $# -gt 1 ]; then WARNING="$2" shift 2 else unknown "-w,--warning requires an argument" fi ;; ######################################## # Special --) shift break ;; -*) unknown "invalid option: ${1}" ;; *) break ;; esac done ################################################################################ # Set COMMON_NAME to hostname if -N was given as argument if [ "$COMMON_NAME" = "__HOST__" ] ; then COMMON_NAME="${HOST}" fi ################################################################################ # Sanity checks ############### # Check options if [ -z "${HOST}" ] ; then usage "No host specified" fi if [ -n "${ALTNAMES}" ] && [ -z "${COMMON_NAME}" ] ; then unknown "--altnames requires a common name to match (--cn or --host-cn)" fi if [ -n "${ROOT_CA}" ] ; then if [ ! -r "${ROOT_CA}" ] ; then unknown "Cannot read root certificate ${ROOT_CA}" fi if [ -d "${ROOT_CA}" ] ; then ROOT_CA="-CApath ${ROOT_CA}" elif [ -f "${ROOT_CA}" ] ; then ROOT_CA="-CAfile ${ROOT_CA}" else unknown "Root certificate of unknown type $(file "${ROOT_CA}" 2> /dev/null)" fi fi if [ -n "${CLIENT_CERT}" ] ; then if [ ! -r "${CLIENT_CERT}" ] ; then unknown "Cannot read client certificate ${CLIENT_CERT}" fi fi if [ -n "${CRITICAL}" ] ; then if ! echo "${CRITICAL}" | grep -q '[0-9][0-9]*' ; then unknown "invalid number of days ${CRITICAL}" fi fi if [ -n "${WARNING}" ] ; then if ! echo "${WARNING}" | grep -q '[0-9][0-9]*' ; then unknown "invalid number of days ${WARNING}" fi fi if [ -n "${CRITICAL}" ] && [ -n "${WARNING}" ] ; then if [ "${WARNING}" -le "${CRITICAL}" ] ; then unknown "--warning (${WARNING}) is less than or equal to --critical (${CRITICAL})" fi fi if [ -n "${TMPDIR}" ] ; then if [ ! -d "${TMPDIR}" ] ; then unknown "${TMPDIR} is not a directory"; fi if [ ! -w "${TMPDIR}" ] ; then unknown "${TMPDIR} is not writable"; fi fi if [ -n "${OPENSSL}" ] ; then if [ ! -x "${OPENSSL}" ] ; then unknown "${OPENSSL} ist not an executable" fi if ! "${OPENSSL}" list-standard-commands | grep -q s_client ; then unknown "${OPENSSL} ist not an openssl executable" fi fi if [ -n "${SSL_LAB_ASSESTMENT}" ] ; then convert_ssl_lab_grade "${SSL_LAB_ASSESTMENT}" SSL_LAB_ASSESTMENT_NUMERIC="${NUMERIC_SSL_LAB_GRADE}" fi if [ -n "${DEBUG}" ] ; then echo "[DBG] ROOT_CA = ${ROOT_CA}" fi ####################### # Check needed programs # OpenSSL if [ -z "${OPENSSL}" ] ; then check_required_prog openssl 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="" if [ -n "${VERBOSE}" ] ; then if [ -z "${EXPECT}" ] ; then echo "expect not available" else echo "expect available (${EXPECT})" fi fi # Timeout (optional) TIMEOUT_BIN="$(which timeout 2> /dev/null)" test -x "${TIMEOUT_BIN}" || TIMEOUT_BIN="" if [ -n "${VERBOSE}" ] ; then if [ -z "${TIMEOUT_BIN}" ] ; then echo "timeout not available" else echo "timeout available (${TIMEOUT_BIN})" fi fi if [ -z "${TIMEOUT_BIN}" ] && [ -z "${EXPECT}" ] && [ -n "${VERBOSE}" ] ; then echo "disabling timeouts" fi PERL="$(which perl 2> /dev/null)" 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 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" fi else 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 if [ -n "${DEBUG}" ] ; then echo "[DBG] check_ssl_version: ${VERSION}" echo "[DBG] OpenSSL binary: ${OPENSSL}" echo "[DBG] OpenSSL version: $( ${OPENSSL} version )" echo "[DBG] System info: $( uname -a )" fi ################################################################################ # Check if openssl s_client supports the -servername option # # openssl s_client does not have a -help option # => We supply an invalid command line option to get the help # on standard error # SERVERNAME= if ${OPENSSL} s_client not_a_real_option 2>&1 | grep -q -- -servername ; 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 echo "'${OPENSSL} s_client' does not support '-servername': disabling virtual server support" fi fi ################################################################################ # Fetch the X.509 certificate # Temporary storage for the certificate and the errors CERT="$( mktemp -t "${0##*/}XXXXXX" 2> /dev/null )" if [ -z "${CERT}" ] || [ ! -w "${CERT}" ] ; then unknown 'temporary file creation failure.' fi ERROR="$( mktemp -t "${0##*/}XXXXXX" 2> /dev/null )" if [ -z "${ERROR}" ] || [ ! -w "${ERROR}" ] ; then unknown 'temporary file creation failure.' fi if [ -n "${OCSP}" ] ; then ISSUER_CERT="$( mktemp -t "${0##*/}XXXXXX" 2> /dev/null )" if [ -z "${ISSUER_CERT}" ] || [ ! -w "${ISSUER_CERT}" ] ; then unknown 'temporary file creation failure.' fi fi if [ -n "${VERBOSE}" ] ; then echo "downloading certificate to ${TMPDIR}" fi CLIENT="" if [ -n "${CLIENT_CERT}" ] ; then CLIENT="-cert ${CLIENT_CERT}" fi CLIENTPASS="" if [ -n "${CLIENT_PASS}" ] ; then CLIENTPASS="-pass pass:${CLIENT_PASS}" fi # Cleanup before program termination # Using named signals to be POSIX compliant trap 'rm -f $CERT $ERROR $ISSUER_CERT' EXIT HUP INT QUIT TERM fetch_certificate if grep -q 'sslv3\ alert\ unexpected\ message' "${ERROR}" ; then if [ -n "${SERVERNAME}" ] ; then # Some OpenSSL versions have problems with the -servername option # We try without if [ -n "${VERBOSE}" ] ; then echo "'${OPENSSL} s_client' returned an error: trying without '-servername'" fi SERVERNAME="" fetch_certificate fi if grep -q 'sslv3\ alert\ unexpected\ message' "${ERROR}" ; then critical "cannot fetch certificate: OpenSSL got an unexpected message" fi fi if ! grep -q "CERTIFICATE" "${CERT}" ; then if [ -n "${FILE}" ] ; then critical "'${FILE}' is not a valid certificate file" else # See # http://stackoverflow.com/questions/1251999/sed-how-can-i-replace-a-newline-n # # - create a branch label via :a # - the N command appends a newline and and the next line of the input # file to the pattern space # - if we are before the last line, branch to the created label $!ba # ($! means not to do it on the last line (as there should be one final newline)) # - finally the substitution replaces every newline with a space on # the pattern space ERROR_MESSAGE="$(sed -e ':a' -e 'N' -e '$!ba' -e 's/\n/; /g' "${ERROR}")" if [ -n "${VERBOSE}" ] ; then echo "Error: ${ERROR_MESSAGE}" fi critical "No certificate returned" fi fi if [ -n "${VERBOSE}" ] ; then echo "parsing the certificate file" fi ################################################################################ # Parse the X.509 certificate DATE="$($OPENSSL x509 -in "${CERT}" -enddate -noout | sed -e "s/^notAfter=//")" # we need to remove everything before 'CN = ', to remove an eventual email supplied with / and additional elements (after ', ') CN="$($OPENSSL x509 -in "${CERT}" -subject -noout -nameopt utf8,oneline,-esc_msb | sed -e "s/^.*[[:space:]]CN[[:space:]]=[[:space:]]//" -e "s/\/[[:alpha:]][[:alpha:]]*=.*\$//" -e "s/,.*//" )" CA_O="$($OPENSSL x509 -in "${CERT}" -issuer -noout | sed -e "s/^.*\/O=//" -e "s/\/[A-Z][A-Z]*=.*\$//")" CA_CN="$($OPENSSL x509 -in "${CERT}" -issuer -noout | sed -e "s/^.*\/CN=//" -e "s/\/[A-Za-z][A-Za-z]*=.*\$//")" SERIAL="$($OPENSSL x509 -in "${CERT}" -serial -noout | sed -e "s/^serial=//")" OCSP_URI="$($OPENSSL x509 -in "${CERT}" -ocsp_uri -noout)" 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 echo "[DBG] " "$($OPENSSL x509 -in "${CERT}" -subject -noout -nameopt utf8,oneline,-esc_msb)" echo "[DBG] CN = ${CN}" echo "[DBG] CA_O = ${CA_O}" echo "[DBG] CA_CN = ${CA_CN}" echo "[DBG] SERIAL = ${SERIAL}" echo "[DBG] OCSP_URI = ${OCSP_URI}" echo "[DBG] ISSUER_URI = ${ISSUER_URI}" echo "[DBG] ${SIGNATURE_ALGORITHM}" fi if echo "${SIGNATURE_ALGORITHM}" | grep -q "sha1" ; then if [ -n "${NOSIGALG}" ] ; then if [ -n "${VERBOSE}" ] ; then echo 'Certificate is signed with SHA-1' fi else critical 'Certificate is signed with SHA-1' fi fi if echo "${SIGNATURE_ALGORITHM}" | grep -qi "md5" ; then if [ -n "${NOSIGALG}" ] ; then if [ -n "${VERBOSE}" ] ; then echo 'Certificate is signed with MD5' fi else critical 'Certificate is signed with MD5' fi fi ################################################################################ # Generate the long output if [ -n "${LONG_OUTPUT_ATTR}" ] ; then check_attr() { ATTR="$1" if ! echo "${VALID_ATTRIBUTES}" | grep -q ",${ATTR}," ; then unknown "Invalid certificate attribute: ${ATTR}" else value="$(${OPENSSL} x509 -in "${CERT}" -noout -nameopt utf8,oneline,-esc_msb -"${ATTR}" | sed -e "s/.*=//")" LONG_OUTPUT="${LONG_OUTPUT}\n${ATTR}: ${value}" fi } # Split on comma if [ "${LONG_OUTPUT_ATTR}" = "all" ] ; then LONG_OUTPUT_ATTR="${VALID_ATTRIBUTES}" fi attributes=$( echo ${LONG_OUTPUT_ATTR} | tr ',' "\n" ) for attribute in $attributes ; do check_attr "${attribute}" done fi ################################################################################ # Compute for how many days the certificate will be valid if [ -n "${DATETYPE}" ]; then CERT_END_DATE=$($OPENSSL x509 -in "${CERT}" -noout -enddate | sed -e "s/.*=//") OLDLANG=$LANG LANG=en_US if [ -n "${DEBUG}" ] ; then echo "[DBG] Date computations: ${DATETYPE}" fi case "${DATETYPE}" in "BSD") DAYS_VALID=$(( ( $(${DATEBIN} -jf "%b %d %T %Y %Z" "${CERT_END_DATE}" +%s) - $(${DATEBIN} +%s) ) / 86400 )) ;; "GNU") DAYS_VALID=$(( ( $(${DATEBIN} -d "${CERT_END_DATE}" +%s) - $(${DATEBIN} +%s) ) / 86400 )) ;; "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 if [ "${DAYS_VALID}" -ge 0 ] ; then echo "The certificate will expire in ${DAYS_VALID} day(s)" else echo "The certificate expired "$((- DAYS_VALID))" day(s) ago" fi fi 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 # 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 [ -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 [ -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 if echo "${COMMON_NAME}" | grep -q -i "^${CN}$" ; then ok="true" fi fi # Check alterante names if [ -n "${ALTNAMES}" ] ; then for cn in ${COMMON_NAME} ; do ok="" if [ -n "${DEBUG}" ] ; then echo "[DBG] checking altnames against ${cn}" fi 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" 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 if [ -n "${DEBUG}" ] ; then echo "[DBG] CN check finished" fi fi ################################################################################ # Check the issuer if [ -n "$ISSUER" ] ; then if [ -n "${DEBUG}" ] ; then echo "[DBG] check ISSUER: ${ISSUER}" fi ok="" CA_ISSUER_MATCHED="" if echo "$CA_CN" | grep -q "^${ISSUER}\$" ; then ok="true" CA_ISSUER_MATCHED="${CA_CN}" fi if echo "$CA_O" | grep -q "^${ISSUER}\$" ; then ok="true" CA_ISSUER_MATCHED="${CA_O}" fi if [ -z "$ok" ] ; then critical "invalid CA ('$ISSUER' does not match '$CA_O' or '$CA_CN')" fi else CA_ISSUER_MATCHED="${CA_CN}" fi ################################################################################ # Check the serial number if [ -n "$SERIAL_LOCK" ] ; then ok="" if echo "$SERIAL" | grep -q "^${SERIAL_LOCK}\$" ; then ok="true" fi if [ -z "$ok" ] ; then critical "invalid serial number ('$SERIAL' does not match '$SERIAL_LOCK')" fi fi ################################################################################ # Check the validity if [ -z "${NOEXP}" ] ; then # We always check expired certificates if ! $OPENSSL x509 -in "${CERT}" -noout -checkend 0 ; then critical "certificate is expired (was valid until $DATE)" fi if [ -n "${CRITICAL}" ] ; then if [ -n "${DEBUG}" ] ; then echo "[DBG] executing: $OPENSSL x509 -in ${CERT} -noout -checkend $(( CRITICAL * 86400 ))" fi if ! $OPENSSL x509 -in "${CERT}" -noout -checkend $(( CRITICAL * 86400 )) ; then critical "certificate will expire on $DATE" fi fi if [ -n "${WARNING}" ] ; then if [ -n "${DEBUG}" ] ; then echo "[DBG] executing: $OPENSSL x509 -in ${CERT} -noout -checkend $(( WARNING * 86400 ))" fi if ! $OPENSSL x509 -in "${CERT}" -noout -checkend $(( WARNING * 86400 )) ; then warning "certificate will expire on $DATE" fi fi fi ################################################################################ # Check SSL Labs if [ -n "${SSL_LAB_ASSESTMENT}" ] ; then if [ -n "${VERBOSE}" ] ; then echo "Checking SSL Labs assestment" fi while true; do JSON="$(curl --silent "https://api.ssllabs.com/api/v2/analyze?host=${HOST}${IGNORE_SSL_LABS_CACHE}")" CURL_RETURN_CODE=$? if [ ${CURL_RETURN_CODE} -ne 0 ] ; then if [ -n "${DEBUG}" ] ; then echo "[DBG] curl returned ${CURL_RETURN_CODE}: curl --silent \"https://api.ssllabs.com/api/v2/analyze?host=${HOST}${IGNORE_SSL_LABS_CACHE}\"" fi unknown "Error checking SSL Labs: curl returned ${CURL_RETURN_CODE}, see 'man curl' for details" fi JSON="$(printf '%s' "${JSON}" | tr '\n' ' ' )" if [ -n "${DEBUG}" ] ; then echo "[DBG] Checking SSL Labs: curl --silent \"https://api.ssllabs.com/api/v2/analyze?host=${HOST}\"" echo "[DBG] SSL Labs JSON: ${JSON}" fi # We clear the cache only on the first run IGNORE_SSL_LABS_CACHE="" SSL_LABS_HOST_STATUS=$(echo "${JSON}" \ | sed 's/.*"status":[ ]*"\([^"]*\)".*/\1/') if [ -n "${DEBUG}" ] ; then echo "[DBG] SSL Labs status: ${SSL_LABS_HOST_STATUS}" fi case "${SSL_LABS_HOST_STATUS}" in 'ERROR') SSL_LABS_STATUS_MESSAGE=$(echo "${JSON}" \ | sed 's/.*"statusMessage":[ ]*"\([^"]*\)".*/\1/') critical "Error checking SSL Labs: ${SSL_LABS_STATUS_MESSAGE}" ;; 'READY') if ! echo "${JSON}" | grep -q "grade" ; then # Something went wrong SSL_LABS_STATUS_MESSAGE=$(echo "${JSON}" \ | sed 's/.*"statusMessage":[ ]*"\([^"]*\)".*/\1/') critical "SSL Labs error: ${SSL_LABS_STATUS_MESSAGE}" else SSL_LABS_HOST_GRADE=$(echo "${JSON}" \ | sed 's/.*"grade":[ ]*"\([^"]*\)".*/\1/') if [ -n "${DEBUG}" ] ; then echo "[DBG] SSL Labs grade: ${SSL_LABS_HOST_GRADE}" fi if [ -n "${VERBOSE}" ] ; then echo "SSL Labs grade: ${SSL_LABS_HOST_GRADE}" fi 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})" fi if [ -n "${DEBUG}" ] ; then echo "[DBG] SSL Labs grade (converted): ${SSL_LABS_HOST_GRADE_NUMERIC}" fi # We have a result: exit break fi ;; 'IN_PROGRESS') # Data not yet available: warn and continue if [ -n "${VERBOSE}" ] ; then echo "Warning: no cached data by SSL Labs, check initiated" fi ;; 'DNS') if [ -n "${VERBOSE}" ] ; then echo 'SSL Labs cannot resolve the domain name' fi ;; *) # Try to extract a message SSL_LABS_ERROR_MESSAGE=$(echo "${JSON}" \ | sed 's/.*"message":[ ]*"\([^"]*\)".*/\1/') if [ -z "${SSL_LABS_ERROR_MESSAGE}" ] ; then SSL_LABS_ERROR_MESSAGE="${JSON}" fi critical "Cannot check status on SSL Labs: ${SSL_LABS_ERROR_MESSAGE}" esac WAIT_TIME=60 if [ -n "${VERBOSE}" ] ; then echo "Waiting ${WAIT_TIME} seconds" fi sleep "${WAIT_TIME}" done fi ################################################################################ # Check revocation via OCSP if [ -n "${OCSP}" ]; then if [ -n "${DEBUG}" ] ; then echo "[DBG] OCSP: fetching issuer certificate ${ISSUER_URI} to ${ISSUER_CERT}" fi curl --silent "${ISSUER_URI}" > "${ISSUER_CERT}" 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 fi if [ -n "${DEBUG}" ] ; then # 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/\/.*//' )" if [ -n "${DEBUG}" ] ; then echo "[DBG] OCSP: host = ${OCSP_HOST}" fi # check if -header is supported OCSP_HEADER="" # 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 # 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 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}" fi if echo "${OCSP_RESP}" | grep -qi "revoked" ; then critical "certificate is revoked" elif ! echo "${OCSP_RESP}" | grep -qi "good" ; then 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 else if [ -n "${VERBOSE}" ] ; then echo "openssl ocsp does not support the -header option: disabling OCSP checks" fi fi fi ################################################################################ # Check the organization if [ -n "$ORGANIZATION" ] ; then ORG=$($OPENSSL x509 -in "${CERT}" -subject -noout | sed -e "s/.*\/O=//" -e "s/\/.*//") if ! echo "$ORG" | grep -q "^$ORGANIZATION" ; then critical "invalid organization ('$ORGANIZATION' does not match '$ORG')" fi fi ################################################################################ # Check the organization if [ -n "$ADDR" ] ; then EMAIL=$($OPENSSL x509 -in "${CERT}" -email -noout) if [ -n "${VERBOSE}" ] ; then echo "checking email (${ADDR}): ${EMAIL}" fi if [ -z "${EMAIL}" ] ; then critical "the certificate does not contain an email address" fi if ! echo "$EMAIL" | grep -q "^$ADDR" ; then critical "invalid email ($ADDR does not match $EMAIL)" fi fi ################################################################################ # Check if the certificate was verified if [ -z "${NOAUTH}" ] && grep -q '^verify\ error:' "${ERROR}" ; then if grep -q '^verify\ error:num=[0-9][0-9]*:self\ signed\ certificate' "${ERROR}" ; then if [ -z "${SELFSIGNED}" ] ; then critical "Cannot verify certificate, self signed certificate" else SELFSIGNEDCERT="self signed " fi else if [ -n "${DEBUG}" ] ; then sed 's/^/[DBG] Error: /' "${ERROR}" fi # Process errors details=$( grep '^verify\ error:' "${ERROR}" | sed 's/verify\ error:num=[0-9]*://' | sed -e ':a' -e 'N' -e '$!ba' -e 's/\n/, /g' ) critical "Cannot verify certificate: ${details}" fi fi ################################################################################ # If we get this far, assume all is well. :) # If --altnames was specified we show the specified CN instead of # the certificate CN if [ -n "${ALTNAMES}" ] && [ -n "${COMMON_NAME}" ] ; then CN=${COMMON_NAME} fi if [ -n "${DAYS_VALID}" ] ; then # nicer formatting if [ "${DAYS_VALID}" -gt 1 ] ; then DAYS_VALID=" (expires in ${DAYS_VALID} days)" elif [ "${DAYS_VALID}" -eq 1 ] ; then DAYS_VALID=" (expires tomorrow)" elif [ "${DAYS_VALID}" -eq 0 ] ; then DAYS_VALID=" (expires today)" elif [ "${DAYS_VALID}" -eq -1 ] ; then DAYS_VALID=" (expired yesterday)" else DAYS_VALID=" (expired ${DAYS_VALID} days ago)" fi fi if [ -n "${SSL_LABS_HOST_GRADE}" ] ; then SSL_LABS_HOST_GRADE=", SSL Labs grade: ${SSL_LABS_HOST_GRADE}" fi echo "${SHORTNAME} OK - X.509 ${SELFSIGNEDCERT}certificate for '${CN}' from '${CA_ISSUER_MATCHED}' valid until ${DATE}${DAYS_VALID}${SSL_LABS_HOST_GRADE}${PERFORMANCE_DATA}${LONG_OUTPUT}" exit 0 } if [ -z "${SOURCE_ONLY}" ]; then main "${@}" fi