#!/bin/bash # # Program: SSL Certificate Check # # Author: Matty < matty at daemons dot net > # # Current Version: 3.1 # # Revision History: # # Version 3.1 # - Added handling for starttls for smtp -- Marco Amrein # - Added handling for starttls for pop3 (without s) -- Marco Amrein # - Removed extra spacing at end of script # # Version 3.0 # - Added "-i" option to print certificate issuer # - Removed $0 from Subject line of outbound e-mails # - Fixed some typographical errors # - Removed redundant "-b" option # # Version 2.0 # - Fixed an issue with e-mails formatting incorrectly # - Added additional space to host column -- Darren-Perot Spruell # - Replaced GNU date dependency with CHRIS F. A. JOHNSON's # date2julian shell function. This routine can be found on # page 170 of Chris's book "Shell Scripting Recipes: A # Problem-Solution Approach," ISBN #1590594711. Julian function # was created based on a post to comp.unix.shell by Tapani Tarvainen. # - Cleaned up function descriptions # - Removed several lines of redundant code # - Adjusted the help message # # Version 1.1 # - Added "-c" flag to report expiration status of a PEM encoded # certificate -- Hampus Lundqvist # - Updated the prints messages to display the reason a connection # failed (connection refused, connection timeout, bad cert, etc) # - Updated the GNU date checking routines # - Added checks for each binary required # - Added checks for connection timeouts # - Added checks for GNU date # - Added a "-h" option # - Cleaned up the documentation # # Version 1.0 # Initial Release # # Last Updated: 12-20-2005 # # Purpose: # ssl-cert-check checks to see if a digital certificate in X.509 format # has expired. ssl-cert-check can be run in interactive and batch mode, # and provides facilities to alarm if a certificate is about to expire. # # License: # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # # Requirements: # Requires openssl # # Installation: # Copy the shell script to a suitable location # # Tested platforms: # -- Solaris 9 using /bin/bash # -- Solaris 10 using /bin/bash # -- OS X 10.4.2 using /bin/sh # -- OpenBSD using /bin/sh # -- FreeBSD using /bin/sh # -- Redhat advanced server 3.0MU3 using /bin/sh # # Usage: # Refer to the usage() sub-routine, or invoke ssl-cert-check # with the "-h" option. # # Examples: # Please refer to the following site for documentation and numerous examples: # http://daemons.net/~matty/articles/checkcertificate.html # PATH=/bin:/usr/bin:/usr/local/bin:/usr/local/ssl/bin:/usr/sfw/bin ; export PATH # Who to page when an expired certificate is detected (cmdline: -e) ADMIN="sysadmin@mydomain.com" # Number of days in the warning threshhold (cmdline: -x) WARNDAYS=30 # If QUIET is set to TRUE, don't print anything on the console (cmdline: -q) QUIET="FALSE" # Don't send emails by default (cmdline: -a) ALARM="FALSE" # Location of system binaries DATE="/bin/date" GREP="/bin/grep" MAIL="/usr/bin/mail" OPENSSL="/usr/bin/openssl" # Place to stash temporary files CERT_TMP="/var/tmp/cert.$$" ERROR_TMP="/var/tmp/error.$$" # Set the default umask to be somewhat restrictive umask 077 ############################################################################# # Purpose: Convert a date from MONTH-DAY-YEAR to Julian format # Acknowledgements: Code was adapted from examples in the book # "Shell Scripting Recipes: A Problem-Solution Approach" # ( ISBN 1590594711 ) # Arguments: # $1 -> Month (e.g., 06) # $2 -> Day (e.g., 08) # $3 -> Year (e.g., 2006) ############################################################################# date2julian() { if [ "${1} != "" ] && [ "${2} != "" ] && [ "${3}" != "" ] then ## Since leap years add aday at the end of February, ## calculations are done from 1 March 0000 (a fictional year) d2j_tmpmonth=$((12 * ${3} + ${1} - 3)) ## If it is not yet March, the year is changed to the previous year d2j_tmpyear=$(( ${d2j_tmpmonth} / 12)) ## The number of days from 1 March 0000 is calculated ## and the number of days from 1 Jan. 4713BC is added echo $(( (734 * ${d2j_tmpmonth} + 15) / 24 - 2 * ${d2j_tmpyear} + ${d2j_tmpyear}/4 - ${d2j_tmpyear}/100 + ${d2j_tmpyear}/400 + $2 + 1721119 )) else echo 0 fi } ############################################################################# # Purpose: Convert a sstring month into a integer representation # Arguments: # $1 -> Month name (e.g., Sep) ############################################################################# getmonth() { case ${1} in Jan) echo 1 ;; Feb) echo 2 ;; Mar) echo 3 ;; Apr) echo 4 ;; May) echo 5 ;; Jun) echo 6 ;; Jul) echo 7 ;; Aug) echo 8 ;; Sep) echo 9 ;; Oct) echo 10 ;; Nov) echo 11 ;; Dec) echo 12 ;; *) echo 0 ;; esac } ############################################################################# # Purpose: Calculate the number of seconds between two dates # Arguments: # $1 -> Date #1 # $2 -> Date #2 ############################################################################# date_diff() { if [ "${1}" != "" ] && [ "${2}" != "" ] then echo $(expr ${2} - ${1}) else echo 0 fi } ##################################################################### # Purpose: Print a line with the expiraton interval # Arguments: # $1 -> Hostname # $2 -> TCP Port # $3 -> Status of certification (e.g., expired or valid) # $4 -> Date when certificate will expire # $5 -> Days left until the certificate will expire # $6 -> Issuer of the certificate ##################################################################### prints() { if [ "${QUIET}" != "TRUE" ] && [ "${ISSUER}" = "TRUE" ] then MIN_DATE=$(echo $4 | awk '{ print $1, $2, $4 }') printf "%-35s %-17s %-8s %-11s %-5s\n" "$1:$2" "$6" "$3" "$MIN_DATE" "$5" elif [ "${QUIET}" != "TRUE" ] then MIN_DATE=$(echo $4 | awk '{ print $1, $2, $4 }') printf "%-47s %-12s %-12s %-6s\n" "$1:$2" "$3" "$MIN_DATE" "$5" fi } #################################################### # Purpose: Print a heading with the relevant columns # Arguments: # None #################################################### print_heading() { if [ "${QUIET}" != "TRUE" ] && [ "${ISSUER}" = "TRUE" ] then printf "\n%-35s %-17s %-8s %-11s %-5s\n" "Host" "Issuer" "Status" "Expires" "Days Left" echo "----------------------------------- ----------------- -------- ----------- ---------" elif [ "${QUIET}" != "TRUE" ] then printf "\n%-47s %-12s %-12s %-6s\n" "Host" "Status" "Expires" "Days Left" echo "----------------------------------------------- ------------ ------------ ----------" fi } ########################################## # Purpose: Describe how the script works # Arguments: # None ########################################## usage() { echo "Usage: $0 [ -e email ] [ -x expir_days ] [ -q ] [ -a ] [ -h ] [-i]" echo " {[ -s common_name ] && [ -p port]} || {-f cert_file} || {-c certificate file}" echo "" echo " -a : Send a warning message through email " echo " -c cert file : Print the expiration date for a PEM formatted" echo " certificate passed as an option" echo " -e email address : Email address to send expiration notices" echo " -f cert file : File with a list of FQDNs and ports" echo " -h : Print this screen" echo " -i : Print the issuer of the certificate" echo " -p port : Port to connect to (interactive mode)" echo " -s commmon name : Server to connect to (interactive mode)" echo " -q : Don't print anything on the console" echo " -x days : Certificate expiration interval (eg. if cert_date < days)" echo "" } ################################################################## # Purpose: Connect to a server ($1) and port ($2) to see if a certificate # has expired # Arguments: # $1 -> Server name # $2 -> TCP port to connect to ################################################################## check_server_status() { if [ "_${2}" = "_pop3" -o "_${2}" = "_143" ] ; then TLSFLAG="-starttls pop3" elif [ "_${2}" = "_smtp" -o "_${2}" = "_25" ] ; then TLSFLAG="-starttls smtp" else TLSFLAG="" fi echo "" | ${OPENSSL} s_client -connect ${1}:${2} ${TLSFLAG} 2> ${ERROR_TMP} 1> ${CERT_TMP} if ${GREP} -i "Connection refused" ${ERROR_TMP} > /dev/null then prints ${1} ${2} "Connection refused" "Unknown" "Unknown" "Unknown" elif ${GREP} -i "gethostbyname failure" ${ERROR_TMP} > /dev/null then prints ${1} ${2} "Cannot resolve domain" "Unknown" "Unknown" "Unknown" elif ${GREP} -i "Operation timed out" ${ERROR_TMP} > /dev/null then prints ${1} ${2} "Operation timed out" "Unknown" "Unknown" "Unknown" elif ${GREP} -i "ssl handshake failure" ${ERROR_TMP} > /dev/null then prints ${1} ${2} "SSL handshake failed" "Unknown" "Unknown" "Unknown" elif ${GREP} -i "connect: Connection timed out" ${ERROR_TMP} > /dev/null then prints ${1} ${2} "Connection timed out" "Unknown" "Unknown" "Unknown" else check_file_status ${CERT_TMP} $1 $2 fi } ##################################################### ### Check the expiration status of a certificate file ### Accepts three parameters: ### $1 -> certificate file to process ### $2 -> Server name ### $3 -> Port number of certificate ##################################################### check_file_status() { HOST=${2} PORT=${3} ### Grab the expiration date from the X.509 certificate CERTDATE=$(${OPENSSL} x509 -in ${1} -enddate -noout | sed 's/notAfter\=//') CERTISSUER=$(${OPENSSL} x509 -in ${1} -issuer -noout | awk 'BEGIN {RS="/" } $0 ~ /^O=/ { print substr($0,3,17)}') ### Split the result into parameters, and pass the relevant pieces to date2julian set -- ${CERTDATE} MONTH=$(getmonth ${1}) # Convert the date to seconds, and get the diff between NOW and the expiration date CERTJULIAN=$(date2julian ${MONTH#0} ${2#0} ${4}) CERTDIFF=$(date_diff ${NOWJULIAN} ${CERTJULIAN}) if [ ${CERTDIFF} -lt 0 ] then if [ "${ALARM}" = "TRUE" ] then echo "The SSL certificate for ${HOST} has expired!" \ | ${MAIL} -s "Certificate for ${HOST} has expired!" ${ADMIN} fi prints ${HOST} ${PORT} "Expired" "${CERTDATE}" "${CERTDIFF}" elif [ ${CERTDIFF} -lt ${WARNDAYS} ] then if [ "${ALARM}" = "TRUE" ] then echo "The SSL certificate for ${HOST} will expire on ${CERTDATE}" \ | ${MAIL} -s "Certificate for ${HOST} will expire in ${WARNDAYS}-days or less" ${ADMIN} fi prints ${HOST} ${PORT} "Expiring" "${CERTDATE}" "${CERTDIFF}" "$CERTISSUER}" else prints ${HOST} ${PORT} "Valid" "${CERTDATE}" "${CERTDIFF}" "${CERTISSUER}" fi } ################################# ### Being the main program logic ################################# while getopts abie:f:c:hp:s:qx: option do case "${option}" in a) ALARM="TRUE";; c) CERTFILE=${OPTARG};; e) ADMIN=${OPTARG};; f) SERVERFILE=$OPTARG;; h) usage exit 1;; i) ISSUER="TRUE";; p) PORT=$OPTARG;; s) HOST=$OPTARG;; q) QUIET="TRUE";; x) WARNDAYS=$OPTARG;; \?) usage exit 1;; esac done ### Check to see if the openssl binary exists if [ ! -f ${OPENSSL} ] then echo "ERROR: The openssl binary does not exist in ${OPENSSL} ." echo " FIX: Please modify the \$OPENSSL variable in the program header." exit 1 fi ### Check to make sure a date utility is available if [ ! -f ${DATE} ] then echo "ERROR: The date binary does not exist in ${DATE} ." echo " FIX: Please modify the \$DATE variable in the program header." exit 1 fi ### Check to make sure a date utility is available if [ ! -f ${GREP} ] then echo "ERROR: The grep binary does not exist in ${GREP} ." echo " FIX: Please modify the \$GREP variable in the program header." exit 1 fi ### CHeck to make sure a mail client is available it automated notifcations are requested if [ "${ALARM}" = "TRUE" ] && [ ! -f ${MAIL} ] then echo "ERROR: You enabled automated alerts, but the mail binary could not be found." echo " FIX: Please modify the \$MAIL variable in the program header." exit 1 fi ### Baseline the dates so we have something to compare to MONTH=$(${DATE} "+%m") DAY=$(${DATE} "+%d") YEAR=$(${DATE} "+%Y") NOWJULIAN=$(date2julian ${MONTH#0} ${DAY#0} ${YEAR}) ### Touch the files prior to using them touch ${CERT_TMP} ${ERROR_TMP} ### If a HOST and PORT were passed on the cmdline, use those values if [ "${HOST}" != "" ] && [ "${PORT}" != "" ] then print_heading check_server_status "${HOST}" "${PORT}" ### If a file and a "-a" are passed on the command line, check all ### of the certificates in the file to see if they are about to expire elif [ -f "${SERVERFILE}" ] then print_heading while read HOST PORT do check_server_status "${HOST}" "${PORT}" done < ${SERVERFILE} ### Check to see if the certificate in CERTFILE is about to expire elif [ "${CERTFILE}" != "" ] then print_heading if [ -f "${CERTFILE}" ] then check_file_status ${CERTFILE} "FILE" "${CERTFILE}" else echo "ERROR: The file named ${CERTFILE} doesn't exist" fi ### There was an error, so print a detailed usage message and exit else usage exit 1 fi ### Remove the temporary files rm -f ${CERT_TMP} ${ERROR_TMP} ### Exit with a success indicator exit 0