diff --git a/check_ssl_cert/Makefile b/check_ssl_cert/Makefile new file mode 100644 index 0000000..cf9673d --- /dev/null +++ b/check_ssl_cert/Makefile @@ -0,0 +1,3 @@ +#/usr/bin/make -f + +include ../common.mk diff --git a/check_ssl_cert/check_ssl_cert b/check_ssl_cert/check_ssl_cert new file mode 100644 index 0000000..87fce2f --- /dev/null +++ b/check_ssl_cert/check_ssl_cert @@ -0,0 +1,1521 @@ +#!/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.31.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 " -f,--file file local file path (works with -H localhost only)" + 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" + 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 " -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: Matteo Corti " + 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() { + + # 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}" + ;; + 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}" + ;; + 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}" + ;; + *) + 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}" + 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}" + + 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 [ $? -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 + +} + + +################################################################################ +# Main +################################################################################ +main() { + + # Default values + DEBUG="" + OPENSSL="" + 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 + ;; + --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 + ;; + -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 + COMMON_NAME="$2" + 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 + + # 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 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" + 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 + + 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}" ] ; then + SERVERNAME="-servername ${COMMON_NAME}" + else + SERVERNAME="-servername ${HOST}" + 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://")" + + 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 "${PERL}" ] ; then + + CERT_END_DATE=$($OPENSSL x509 -in "${CERT}" -noout -enddate | sed -e "s/.*=//") + + 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 + ) + + 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 + 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 + + # Match the domain + if echo "${COMMON_NAME}" | grep -q "^$(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] the common name ${COMMON_NAME} matches ^$(echo "${CN}" | sed -e 's/[.]/[.]/g' -e 's/[*]/[A-Za-z0-9\-]*/' )\$" + fi + ok="true" + fi + + else + + case "${COMMON_NAME}" in + ${CN}) + ok="true" + ;; + esac + + fi + + # Check alterante names + if [ -n "${ALTNAMES}" ] ; then + + if [ -n "${DEBUG}" ] ; then + echo "[DBG] checking altnames" + 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 + + case "$COMMON_NAME" in + "$alt_name") + ok="true" + ;; + esac + + done + + 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}" + + # 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 file "${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}" + fi + + if [ -n "${DEBUG}" ] ; then + echo "[DBG] OCSP: storing a copy of the retrieved issuer certificate to ${ISSUER_URI##*/}" + cp "${ISSUER_CERT}" "${ISSUER_URI##*/}" + 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="" + if "${OPENSSL}" ocsp 2>&1 | grep -q -- -header ; 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}" + 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 "${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 + + OCSP_RESP="$($OPENSSL ocsp -no_nonce -issuer "${ISSUER_CERT}" -cert "${CERT}" -url "${OCSP_URI}" "${OCSP_HEADER}" 2>&1 )" + 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 diff --git a/check_ssl_cert/control b/check_ssl_cert/control new file mode 100644 index 0000000..cae2971 --- /dev/null +++ b/check_ssl_cert/control @@ -0,0 +1,9 @@ +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 +Uploaders: Jan Wagner +Description: plugin checking 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 diff --git a/check_ssl_cert/copyright b/check_ssl_cert/copyright new file mode 100644 index 0000000..6e61fa6 --- /dev/null +++ b/check_ssl_cert/copyright @@ -0,0 +1,8 @@ +Copyright (c) 2007-2013 ETH Zurich +Copyright (c) 2007-2016 Matteo Corti + +License: GPL v3 + + On Debian systems, the complete text of the GNU General + Public License version 3 can be found in "/usr/share/common-licenses/GPL-3". +