#!/bin/bash
#
# Program: SSL Certificate Check <ssl-cert-check>
#
# Author: Matty <matty@daemons.net>
#
# Current Version: 1.4
#
# Revision History:
#
#   Version 1.4
#      Added "-c" flag to report expiration status of a PEM encoded certificate 
#        -- functionality added by Hampus Lundqvist
#
#   Version 1.3
#      Updated the prints messages to display the reason a connection
#      failed (connection refused, connection timeout, bad cert, etc)
#
#      Updated the GNU date checking routines. I really wish UNIX/BSD/Linux
#      based systems utilized a common set of options/interfaces. *sigh*
#
#   Version 1.2
#      Added checks for each binary required
#      Added checks for connection timeouts
#
#   Versions 1.1
#      Added checks for GNU date
#      Added a "-h" option
#      Cleaned up the documentation
#
#  Version 1.0
#      Initial Release
#
# Last Updated: 01-04-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 free software; you can redistribute it and/or modify it
#   under the terms of the GNU General Public License as published by the
#   Free Software Foundation; either version 2, or (at your option) any
#   later version.
#
#   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.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program; if not, write to the Free Software
#   Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
# Requirements:
#   Requires openssl and GNU date
# 
# Installation: 
#   Copy the shell script to a suitable location
# 
# Usage (script should be run as a non-privileged user): 
#  Usage: ./ssl-cert-check {[ -c certificate file ]} || {[ -b ] && [ -f cert_file ]} || {[ -s common_name ] && [ -p port]}}
#             [ -e email ] [ -x expir_days ] [ -q ] [ -a ] [ -h ]
#    -a               : Send a warning message through email 
#    -b               : Print the expiration date for all certificates in cert_file (batch mode)
#    -c               : Print the expiration date for a PEM formatted certificate
#    -e email address : Email address to send expiration notices
#    -f cert file     : File with a list of common names and ports (eg., blatch.com 443)
#    -h               : Print this screen
#    -p port          : Port to connect to (interactive mode)
#    -s commmon name  : Server to connect to (interactive mode)
#    -q               : Don't print anything on the console
#    -x days          : Certificate expiration interval (eg. if cert_date < days)
#  
# 
# Examples:
#   Print the certificate expiration status of the X.509 certificate stored in file cacert.pem
#    $ ssl-cert-check -c /etc/ca/cacert.pem
#
#   Print the certificate expiration dates for a list of domains specified in the file ssldomains:
#    $ ssl-cert-check -b -f ssldomains
# 
#  Print the certificate expiration date for mail.daemons.net:
#    $ ssl-cert-check -s mail.daemons.net -p 995 
#
#  Run ssl-cert-check in quiet mode, check for all certificates that will
#  expire in 75-days or less. Email the expiring certs to matty@daemons.net:
#    $ ssl-cert-check -a -b -f ssldomains -q -x 75 -e matty@daemons.net

PATH=/bin:/usr/bin:/usr/local/bin:/usr/local/ssl/bin ; export PATH

# Who to page when an expired certificate is detected (cmdline: -e)
ADMIN="sysadmin@mydomain.com"

# Number of days to give as a buffer  (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"
MAIL="/bin/mail"
OPENSSL="/usr/bin/openssl"

# Place to stash temporary files
CERT_TMP="$HOME/cert.$$"
ERROR_TMP="$HOME/error.$$"

# Set the default umask to be somewhat restrictive
umask 077


###########################################################
### Converts a date in string format passed as $1 into
### the number of seconds since 01/01/70 00:00:00 UTC.
### Replace this with another date normalization routine if
### you do not have GNU date available.
###########################################################
utcseconds() {
        utcsec=`${DATE} -d "$1" +%s`
        echo "$utcsec"
}


###########################################################
### Calculate the number of seconds between two dates
###########################################################
date_diff() {
        diff=`expr $2 - $1`
        echo "${diff}"
}


###########################################################
### Calculate days given seconds
###########################################################
date_days() {
	DAYS=`expr ${1} \/ 86400`
	echo "${DAYS}"
}


#####################################################################
### Method used to print a line with a certificates expiration status
#####################################################################
prints() {
	if [ "${QUIET}" != "TRUE" ]
	then
		MIN_DATE=`echo $4 | awk '{ print $1, $2, $4 }'`
		printf "%-30s %-25s %-20s %-5s\n" "$1:$2" "$3" "$MIN_DATE" "$5"
	fi
}


########################
### Print out a heading 
########################
print_heading() {
	if [ "${QUIET}" != "TRUE" ]
	then
		printf "\n%-30s %-25s %-20s %-5s\n" "Host" "Status" "Expires" "Days Left"
	fi
}


##########################################
# Provide a listing of how the tool works
##########################################
useage() {
	echo "Usage: $0 {[ -c certificate file ]} || {[ -b ] && [ -f cert_file ]} || {[ -s common_name ] && [ -p port]}}"
	echo "           [ -e email ] [ -x expir_days ] [ -q ] [ -a ] [ -h ]"
	echo "  -a               : Send a warning message through email "
	echo "  -b               : Print the expiration date for all certificates in cert_file (batch mode)"
	echo "  -c               : Print the expiration date for a PEM formatted certificate"
	echo "  -e email address : Email address to send expiration notices"
	echo "  -f cert file     : File with a list of common names and ports (eg., blatch.com 443)"
	echo "  -h               : Print this screen"
	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)"
}


##################################################################
### Connect to a server ($1) and port ($2) to see if a certificate
### has expired
##################################################################
check_server_status() {

	echo "" | ${OPENSSL} s_client -connect ${1}:${2} 2> ${ERROR_TMP} 1> ${CERT_TMP}

        if grep -i  "Connection refused" ${ERROR_TMP} > /dev/null
        then
                prints ${1} ${2} "Connection refused" "?"  "?"

        elif grep -i "gethostbyname failure" ${ERROR_TMP} > /dev/null
        then
                prints ${1} ${2} "Cannot resolve domain" "?"  "?"

        elif grep -i "Operation timed out" ${ERROR_TMP} > /dev/null
        then
                prints ${1} ${2} "Operation timed out" "?"  "?"

        elif grep -i "ssl handshake failure" ${ERROR_TMP} > /dev/null
        then
                prints ${1} ${2} "SSL handshake failed" "?"  "?"

	elif grep -i "connect: Connection timed out" ${ERROR_TMP} > /dev/null
        then
		prints ${1} ${2} "Connection timed out" "?"  "?"

        else
		check_expiration ${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_expiration() {

       	CERTDATE=`${OPENSSL} x509 -in ${1} -enddate -noout | sed 's/notAfter\=//'`

	# Convert the date to seconds, and get the diff between NOW and the expiration date
        CERTUTC=`utcseconds "${CERTDATE}"`
        CERTDIFF=`date_diff ${NOWUTC} ${CERTUTC}`

        if [ ${CERTDIFF} -lt 0 ]
        then
 		if [ "${ALARM}" == "TRUE" ]
		then
                       	echo "The SSL certificate for ${2} has expired!" \
                       	| ${MAIL} -s "Certificate for ${2} has expired!" ${ADMIN}
		fi

		DAYS=`date_days $CERTDIFF`
                prints ${2} ${3} "Expired" "$CERTDATE" "$DAYS"

        elif [ ${CERTDIFF} -lt ${WARNSECS} ]
        then
		if [ "${ALARM}" == "TRUE" ]
		then
                       	echo "The SSL certificate for ${2} will expire on ${CERTDATE}" \
                       	| ${MAIL} -s "$0: Certificate for ${2} will expire in ${WARNDAYS}-days or less" ${ADMIN}
		fi
		
		DAYS=`date_days $CERTDIFF`
               	prints ${2} ${3} "Expiring" "${CERTDATE}" "${DAYS}"

        else
                DAYS=`date_days $CERTDIFF`
                prints ${2} ${3} "Valid" "${CERTDATE}"  "${DAYS}"

        fi
}


#################################
### Being the main program logic
#################################
while getopts abe:f:c:hp:s:qx: option
do
        case "${option}"
        in
                a) ALARM="TRUE";;
                b) REPORT_ALL="TRUE";;
		c) CERTFILE=${OPTARG};;
		e) ADMIN=${OPTARG};;
                f) SERVERFILE=$OPTARG;;
		h) useage
		   exit 1;;
                p) PORT=$OPTARG;;
		s) HOST=$OPTARG;;
		q) QUIET="TRUE";;
		x) WARNDAYS=$OPTARG;;
                \?) useage
		    exit 1;;
        esac
done


if [ ! -f ${OPENSSL} ]
then
	echo "ERROR: $OPENSSL does not exist. Please modify the \$OPENSSL variable."
	exit 1
fi

if [ ! -f ${DATE} ]
then
        echo "ERROR: $DATE does not exist. Please modify the \$DATE variable."
	exit 1
fi

if [ ! -f ${MAIL} ]
then
        echo "ERROR: $MAIL does not exist. Please modify the \$MAIL variable."
	exit 1
fi

if ${DATE} -R -d "Wed Jan 1 00:00:00 EDT 2000" "+%s" 2>&1 | grep -i "illegal option" > /dev/null
then
	echo "ERROR: \$DATE does not point to GNU date"
	exit 1
fi

if ${DATE} -R -d "Wed Jan 1 00:00:00 EDT 2000" "+%s" 2>&1 | grep -i "unknown option" > /dev/null
then
        echo "ERROR: \$DATE does not point to GNU date"
        exit 1
fi

### Baseline the dates so we have something to compare with
NOWDATE=`${DATE}`
NOWUTC=`utcseconds "${NOWDATE}"`


### Get the number of seconds to compare the UTC time with 
WARNSECS=`expr ${WARNDAYS} \* 86400`


### Touch the files to avoid issues
touch ${CERT_TMP} ${ERROR_TMP}


### IF a HOST and PORT were passed on the cmdline, use that
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 [ "${REPORT_ALL}" == "TRUE" ] && [ -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_expiration ${CERTFILE} "FILE"  "${CERTFILE}"
	else
		echo "ERROR: The file named ${CERTFILE} doesn't exist"
	fi

###################################################
### There was an error, so print how the tool works
###################################################
else
	useage
fi

###############################
### Remove the temporary files
###############################
rm -f ${CERT_TMP} ${ERROR_TMP}
