a shell script for review.

I have written a bit of shell that lets our company check all our SSL certs.

the aim is to have a list of servers and run this check from cron once a week.

Our managers have decided that we will not run BASH, so it has been written in /bin/sh and only needs openssl, no perl, no bash, no extra packages.

I look foward to input / review / comments.
there is nothing better for learning that have someone show you why your code is bad...

if you can find use for it in your company then feel free to use it.

#!/bin/sh
######################################
#
# SSL testing tool.
#
# Derek Robson  03/10/2009
# used to test if an SSL cert is due to expire.
#  SSL_test.sh <server name>
#
#
######################################
#
# CONFIG SETTINGS
#
# Who to email if SSL cert is due 
EMAIL="bob@mycomany.com"

# how manys days before cert expires do we want to be alerted?
DAYS_WARNING=45

# Also email the cert owner?
MAIL_OWNER=NO

# if no SSL is running on server should we exit with what error code? 
NO_SSL=0;



date2unix() {
        # unix date from Gregorian calendar date
        # Since leap years add aday at the end of February, 
        # calculations are done from 1 March 0000 (a fictional year)
        d2j_tmpmonth=`expr 12 \* ${3} + ${1} - 3` 
        
        # If it is not yet March, the year is changed to the previous year
        d2j_tmpyear=`expr ${d2j_tmpmonth} / 12`
        
        # The number of days from 1 March 0000 is calculated
        # and the number of days from 1 Jan. 4713BC is added 
        d2j_tmpdays=`expr 734 \* ${d2j_tmpmonth} + 15`
 
        # this gives us the julian day number.
        d2j_JULIAN=`expr \( ${d2j_tmpdays} / 24 \) - \( 2 \* ${d2j_tmpyear} \) + \( ${d2j_tmpyear} / 4 \) - \( ${d2j_tmpyear} / 100 \) + \( ${d2j_tmpyear} / 400 \) + $2 + 1721119`

        # this convert julian day number to classic unix time number
        UNIX_date=`expr \( \( \( \( \( ${d2j_JULIAN} - 2440587 \) \* 24 \) + 12 + ${4} \) \* 60 \) + ${5} \) \* 60 `

        echo $UNIX_date 
}





GET_EXPIR_DATE() {
        #
        # get the SSL expire date from the web server.
        # returns a line like this "Not After : Nov 12 12:00:00 2011 GMT"
        # can be used on other port numbers by use of $2, assumes 443 if no $2 set.

        # what host are we checking?
        HOST=$1

        # What port number?
        PORT=443 
        
        # connect to server and get full SSL cert.
        FULL_SSL_BLOB=`echo "HEAD / HTTP/1.0\n Host: $1:443\n\n EOT\n" | openssl s_client -connect $1:443 2>&1`
        if [ $? -eq 0 ];then
        # cut out just the encoded SSL cert
        SSL_CERT=`echo "$FULL_SSL_BLOB" | sed -n '/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/p' `

        # decode the SSL cert recover the "Not After" date      
        EXPIR_DATE=`echo "$SSL_CERT" | openssl x509 -noout -text -certopt no_signame 2>&1 | grep "Not After"  `
        
        echo $EXPIR_DATE
        else 
        echo "ERROR"
        fi 
}



GET_EXPIR_DATE_BUG() {
        #
        # get the SSL expire date from the web server and use works around an  SSL bug with some certs.
        # returns a line like this "Not After : Nov 12 12:00:00 2011 GMT"
        # can be used on other port numbers by use of $2, assumes 443 if no $2 set.

        # what host are we checking?
        HOST=$1

        # What port number?
        PORT=443 
 
        # connect to server and get full SSL cert.
        FULL_SSL_BLOB=`echo "HEAD / HTTP/1.0\n Host: $1:443\n\n EOT\n" | openssl s_client -prexit -connect $1:443 2>&1`
        if [ $? -eq 0 ];then
        # cut out just the encoded SSL cert
        SSL_CERT=`echo "$FULL_SSL_BLOB" | sed -n '/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/p' `

        # decode the SSL cert recover the "Not After" date
        EXPIR_DATE=`echo "$SSL_CERT" | openssl x509 -noout -text -certopt no_signame 2>&1 | grep "Not After"  `
        
        echo $EXPIR_DATE
        else
        echo "ERROR"
        fi
}




GET_CERT_OWNER() {
        #
        # get the SSL owners email address from the web server.
        # returns an emial address
        # can be used on other port numbers by use of $2, assumes 443 if no $2 set.

        # what host are we checking?
        HOST=$1

        # What port number?
        PORT=443

        # connect to server and get full SSL cert.
        FULL_SSL_BLOB=`echo "HEAD / HTTP/1.0\n Host: $1:443\n\n EOT\n" | openssl s_client -prexit -connect $1:443 2>&1`

        # cut out just the encoded SSL cert
        SSL_CERT=`echo "$FULL_SSL_BLOB" | sed -n '/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/p' `

        # decode the SSL cert recover the "Not After" date
        SSL_OWNER=`echo "$SSL_CERT" | openssl x509 -noout -text -certopt no_signame | grep "email" | sed -e 's/email://g'  `

        echo $SSL_OWNER
}




GET_MONTH() 
{
        # convert month name to number
    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
}



#################### MAIN ###########################


         # setup with todays date/time
         NOW_YEAR=`date "+%Y"`
         NOW_MONTH=`date "+%m"`
         NOW_DAY=`date "+%d"`
         NOW_HOUR=`date "+%H"`
         NOW_MINUTE=`date "+%M"`

         # get the expire date of the SSL cert from server.
         BOB=`GET_EXPIR_DATE $1 $2`
         if [ "$BOB" = "" ];then
         # assume we need to use the SSL bug switch 
         BOB=`GET_EXPIR_DATE_BUG $1 $2`
         fi

         if [ "$BOB" = "ERROR" ];then
         exit $NO_SSL
         else         
         # take date from SSL cert and cut down to year, month, day, hour and minute
         SSL_YEAR=`echo $BOB | cut -f 7 -d " "`
         SSL_Month=`echo $BOB | cut -f 4 -d " "`
         # conver month from name to number         
         SSL_MONTH=`GET_MONTH $SSL_Month`
         SSL_DAY=`echo $BOB | cut -f 5 -d " "`
         SSL_HOUR=`echo $BOB | cut -f 6 -d " "| cut -f 1 -d ":"`
         SSL_MINUTE=`echo $BOB | cut -f 6 -d " "| cut -f 2 -d ":"`

         # convert both todays date and SSL cert dat to unix time.
         SSL_DATE=`date2unix $SSL_MONTH $SSL_DAY $SSL_YEAR $SSL_HOUR $SSL_MINUTE`
         NOW_DATE=`date2unix $NOW_MONTH $NOW_DAY $NOW_YEAR $NOW_HOUR $NOW_MINUTE`

         #calculate how many day to go until cert expires.
         DATE_DIFFRANCE=`expr $SSL_DATE - $NOW_DATE`
         DAY_DIFFRANCE=`expr $DATE_DIFFRANCE / 86400`

         # take action if needed.
         if [ $DAY_DIFFRANCE -lt $DAYS_WARNING ]; then

         OWNER=`GET_CERT_OWNER $1 $2`

         MESSG="The SSL certificate for $1 on port $2 will expire on $SSL_DAY/$SSL_MONTH/$SSL_YEAR\n\n"

         echo $MESSG | mailx -s "SSL cert due to expire" $EMAIL 

         if [ "$MAIL_OWNER" = "YES" ] ;then
         echo $MESSG | mailx -s "SSL cert due to expire" $OWNER
         fi

         else

         exit 0

         fi

         fi

Nice approach to resolving the number of days; I have never seen it tackled that way.

I will play with it later.

Thanks for sharing it.

many date solutions use perl, which we did not want to use.
others use GNU date, which we don't have on many of our systems.

one possible problem is that this code is not time-zone aware yet, SSL dates are in GMT, the compare date is local system time.
this would only be an issue is you set the alert days to less than 2 or 3.

the code still needs error checking, input validation, usage notes, more comments, and some other clean-up...

this is just the first working version :slight_smile:

already we have found several SSL cert's that need re-issuing.

Why are you using expr? The standard Unix shell has arithmetic built in.

See the shell date functions at The Dating Game.

... ...

Calling date five times is inefficient and will fail if a time boundary is crossed between calls to date.

 eval "$( date +"NOW_YEAR=+%Y NOW_MONTH=%m NOW_DAY=+%d NOW_HOUR=%H NOW_MINUTE=%M)"

I saw similar code somewhere and simplified (good for a few hundred years) it to (in bash):

(( ++mon < 4 )) && let year-=1 mon+=12
let j='year*1461/4+mon*153/5+day'
let s='(((j-719606)*24+hour)*60+min)*60+sec'

Translated to portable syntax that will run in any POSIX shell:

mon=$(( $mon + 1 ))
if [ $mon -lt 4 ]
then
   year=$(( $year - 1 ))
   mon=$(( mon + 12 ))
fi
j=$(( $year * 1461 / 4 + mon * 153 / 5 + $day ))
s=$(( ( ( (j-719606) * 24 + $hour ) * 60 + $min ) * 60 + $sec ))

I don't think you can do the $(( syntax in bourne shell...

my code was written for bourne shell as we dont run ksh or bash in our network.

Very nice! I did this in perl a while back but an sh version is very cool :cool:

I found a couple of places where there's a little bit of room for simplification, pasted snips from my old solution as examples.

  • [quote=""robsonde"]

    text FULL_SSL_BLOB=`echo "HEAD / HTTP/1.0\n Host: $1:443\n\n EOT\n" | openssl s_client -connect $1:443 2>&1`

    [/quote]


    I found I could just echo a blank line to s_client for this:
    text `echo | $OPENSSL s_client -connect ${server}:${port} 2>&1`

  • [quote="robsonde"]

    text `echo "$SSL_CERT" | openssl x509 -noout -text -certopt no_signame 2>&1 | grep "Not After" `

    [/quote]

    You can also save a cycle or three by using the x509 module's 'enddate' option:
    text `echo "$cert" | $OPENSSL x509 -enddate -noout`

Might I also suggest you don't force it to use port 443 and allow user-specified ports ah-la a plain old URL on the commandline.
That way you can use it to test things like weblogic / websphere internal SSL certs - the kind of things that often get forgotten :slight_smile: (That's what my one got written to test for)

This is also a very under-used feature of many commercial monitoring apps out there to do similar, take a look at the webserver monitoring modules on your monitoring app of choice. It might save you some support overhead.

---------- Post updated at 05:13 PM ---------- Previous update was at 05:11 PM ----------

Bravo! :wink:

It runs in the standard Unix shell, which is a POSIX shell.

The Bourne shell is obsolete, and, while a few systems still have a Bourne shell in /bin/sh, they will also have a conforming shell.

The conforming shell may not be either ksh or bash.