#!/bin/sh
#
# Run 'openssl x509' or 'certtool -i' on all certificate files
# Returns nonzero on failure. Fails if cert cannot be read or is older than
# $SOON (default 60).
#
# Hard dependency: OpenSSL OR gnutls
# Recommended: GNU date
#
# zzz 2011-08
# kytv 2013-03
# public domain
#

# How soon is too soon for a cert to expire?
# By default <= 60 will fail. 90 < x < 60 will warn.
WARN=90
SOON=10

date2julian() {
  # Julian date conversion adapted from a post (its code released into the public
  # domain) by Tapani Tarvainen to comp.unix.shell 1998)) for portability
  # (e.g. using 'expr' instead of requiring Bash, ksh, or zsh).
  #   $1 = Month
  #   $2 = Day
  #   $3 = Year

  if [ "${1}" != "" ] && [ "${2}" != "" ] && [ "${3}" != "" ]; then
    ## Because leap years add a day 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
    expr \( 734 \* ${d2j_tmpmonth} + 15 \) / 24 - 2 \* ${d2j_tmpyear} + ${d2j_tmpyear} / 4 - ${d2j_tmpyear} / 100 + ${d2j_tmpyear} / 400 + $2 + 1721119
  else
    # We *really* shouldn't get here
    echo 0
  fi
}

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
}

checkcert() {
  if [ $OPENSSL ]; then
    # OpenSSL's format: Mar  7 16:08:35 2022 GMT
    DATA=$(openssl x509 -enddate -noout -in $1 | cut -d'=' -f2-)
  else
    # Certtool's format: Mon Mar 07 16:08:35 UTC 2022
    DATA=$(certtool -i <"$1" | sed -e '/Not\sAfter/!d' -e 's/^.*:\s\(.*\)/\1/')
    # The formatting is normalized for passing to the date2julian function (if needed)
    set -- $DATA
    DATA="$2 $3 $4 $6 GMT"
  fi
  echo $DATA
}

get_bits() {
  if [ $OPENSSL ]; then
    BITS=$(openssl x509 -text -noout -in $1 | sed -e '/Public-Key/!d' \
      -e 's/\s\+Public-Key: (\([0-9]\+\) bit)/\1 bits/')
  else
    BITS=$(certtool -i <$1 | sed -e '/^.*Algorithm Security Level/!d' \
      -e 's/.*(\([0-9]\+\) bits).*/\1 bits/')
  fi
}

get_sigtype() {
  if [ $OPENSSL ]; then
    TYPE=$(openssl x509 -text -noout -in $1 | sed -e '/Signature Algorithm/!d' \
      -e 's/\s\+Signature Algorithm:\s\+\(.\+\)/\1/' | head -n1)
  else
    TYPE=$(certtool -i <$1 | sed -e '/^.*Signature Algorithm:/!d' \
      -e 's/.*:\s\+\(.*\)/\1/')
  fi
}

print_status() {
  if [ $DAYS -ge $SOON ]; then
    echo "> Expires in $DAYS days ($EXPIRES)"
  elif [ $DAYS -eq 1 ]; then
    DAYS=$(echo $DAYS | sed 's/^-//')
    echo "! Check for $I failed, expires tomorrow ($EXPIRES)"
    FAIL=1
  elif [ $DAYS -eq 0 ]; then
    echo "! Check for $i failed, expires today ($EXPIRES)"
    FAIL=1
  elif [ $DAYS -le $SOON ] && [ $DAYS -gt 0 ]; then
    echo "! Check for $i failed, expires in $DAYS days (<= ${SOON}d) ($EXPIRES)"
    FAIL=1
  elif [ $DAYS -le $WARN ] && [ $DAYS -gt $SOON ]; then
    echo "! WARNING: $i expires in $DAYS days (<= ${WANT}d) ($EXPIRES)"
  elif [ $DAYS -lt 0 ]; then
    DAYS=$(echo $DAYS | sed 's/^-//')
    echo "! Check for $i failed, expired $DAYS days ago ($EXPIRES)"
    FAIL=1
  fi
}

compute_dates() {
  if [ -n "$HAVE_GNUDATE" ]; then
    SECS=$(date -u -d "$EXPIRES" '+%s')
    DAYS="$(expr \( $SECS - $NOW \) / 86400)"
  else
    set -- $EXPIRES
    # date2julian needs the format mm dd yyyy
    SECS=$(date2julian $(getmonth $1) $2 $4)
    DAYS=$(expr $SECS - $NOW)
  fi
  print_status
}

# This "grouping hack" is here to prevent errors from being displayed with the
# original Bourne shell (Linux shells don't need the {}s
if { date --help; } >/dev/null 2>&1; then
  HAVE_GNUDATE=1
  NOW=$(date -u '+%s')
else
  NOW=$(date2julian $(date -u '+%m %d %Y'))
fi

if [ $(which openssl) ]; then
  OPENSSL=1
elif [ $(which certtool) ]; then :; else
  echo "! ERROR: Neither certtool nor openssl were found..." >&2
  exit 1
fi

cd $(dirname $0)/../../installer/resources/certificates

for i in */*.crt; do
  echo "> Checking $i ..."
  EXPIRES=$(checkcert $i)
  if [ -z "$EXPIRES" ]; then
    echo "! FAILED CHECK FOR $i"
    FAIL=1
  else
    compute_dates
  fi
  get_bits $i && get_sigtype $i
  printf '%s - %s\n\n' "$BITS" "$TYPE"
  if grep '\s$' $i >/dev/null 2>&1; then
    echo "! Trailing whitespace found in file $i"
    FAIL=1
  fi
  if grep '^\s' $i >/dev/null 2>&1; then
    echo "! Leading whitespace found in file $i"
    FAIL=1
  fi
done

if [ -n "$FAIL" ]; then
  echo "! At least one file failed certificate validity check"
else
  echo "All files passed certificate validity check"
fi

[ -n $FAIL ] && exit $FAIL
