#! /bin/bash

progname=$(basename $0)

OPT_SUMMARY=0
OPT_SOME=1
OPT_MOST=2
OPT_ALL=3

EXIT_SUCCESS=0
EXIT_FILE_OPEN=1
EXIT_CHECK=2
EXIT_HELP=3

declare -a options

summaryopts="-subject -dates -issuer -noout"
someopts="-text -fingerprint -noout -certopt no_pubkey,no_sigdump,no_version,no_signame,no_serial"
mostopts="-text -fingerprint -noout -certopt no_pubkey,no_sigdump"
allopts="-text -fingerprint -noout"

showDetails=$OPT_SUMMARY
doCheck=0
exitStatus=$EXIT_SUCCESS

options[$OPT_SUMMARY]=$summaryopts
options[$OPT_SOME]=$someopts
options[$OPT_MOST]=$mostopts
options[$OPT_ALL]=$allopts

# Set the exit status to indicate a consistency problem.
# This is a function in case we decide that we don't want
# to overwrite a more serious error.
SetCheckError ()
{
	exitStatus=$EXIT_CHECK
}

x509_show_cert ()
{
	local cert="$1"
	shift
	local opts=$(eval echo $*)

	eval openssl x509 $opts <<< "$cert"
}

x509_show_chain ()
{
	local filename="$1"
	local showDetails=$2
	local line=
	local cert=
	local newline=$'\n'
	local firstInFile=1
	shift
	shift
	local opts=$*

	local line=
	local line2=
	local numCertsInFile=0
	local lastIdx=0
	local numSelfSigned=0
	local seenSelfSignedIdx=0
	local seenExtraCert=0
	local seenCA=0
	local extraCert=0
	local curIdx=0
	local curSubj=""
	local curIssuer=""
	local lastIdx=0
	local lastSubj=""
	local lastIssuer=""
	local nl=$'\n'		# set to "" when not wanted
	local NL=$'\n'		# never changed

	IFS=$'\n'
	while read line
	do
		if [[ "$line" =~ BEGIN.*CERTIFICATE ]]
		then
			cert="$line"

			while read line
			do
				cert+="$newline$line"
				if [[ "$line" =~ END.*CERTIFICATE ]]
				then
					if [ $firstInFile -eq 1 ]
					then
						firstInFile=0
					else
						echo "***"
					fi
					certInfo=$(x509_show_cert "$cert" $opts)
					echo "Cert #${curIdx}:"
					echo "${certInfo}"

					((++numCertsInFile))

					# get and check Subject and Issuer from this cert
					while read line2
					do
						case "${line2}" in
						subject=*)
							curSubj=$(echo "${line2}" | sed -e 's/^subject=//')
							;;
						issuer=*)
							curIssuer=$(echo "${line2}" | sed -e 's/^issuer=//')
							;;
						esac
					done <<< "${certInfo}"

					#
					# check for errors or anomalies
					#
					if [ $doCheck -ne 0 ]
					then
						nl=$'\n'
						if [ ${numSelfSigned} -ne 0 ]
						then
							seenExtraCert=1
							SetCheckError
							echo "${nl}-- Cert #${curIdx} is after self-signed cert #${seenSelfSignedIdx}, so OpenSSL will ignore it."
							nl=""
						fi

						if [ ${curIdx} -gt 0 ]
						then
							if [ "${lastIssuer}" != "${curSubj}" ]
							then
								SetCheckError
								echo "${nl}-- Cert #${curIdx} did not sign the previous cert #${lastIdx}."
								echo "   #${lastIdx}: Previous Issuer=\"${lastIssuer}\""
								echo "   #${curIdx}: Current Subject=\"${curSubj}\""
								nl=""
							fi
						fi

						if [ -n "${curIssuer}" -a "${curIssuer}" = "${curSubj}" ]
						then
							((++numSelfSigned))
							seenSelfSignedIdx=${curIdx}
							echo "${nl}-- Cert #${curIdx} is self-signed."
							nl=""
						fi

						lastSubj="${curSubj}"
						lastIssuer="${curIssuer}"

						curSubj=""
						curIssuer=""
					fi # doCheck

					lastIdx=${curIdx}
					((++curIdx))
					cert=
				fi
			done
		fi
	done < "$filename"
	exitStatus=$?

	if [ $doCheck -ne 0 ]
	then
		nl=$'\n'
		if [ $numSelfSigned -ne 0 ]
		then
			local lastIdx=$((numCertsInFile - 1))
			[ $lastIdx -lt 0 ] && lastIdx=0
			echo "${nl}>> File \"${filename}\":"
			echo "   -- contains ${numSelfSigned} self-signed certificate(s);"
			echo "   -- the last self-signed certificate is #${seenSelfSignedIdx};"
			echo "   -- the last certificate in the file is #${lastIdx}."
		fi

		if [ ${numSelfSigned} -ne 0 ]
		then
			[ ${seenSelfSignedIdx} -ne ${lastIdx} ] && SetCheckError
		fi
	fi
}

CurDetail ()
{
	local val=$(($1 == $showDetails))

	echo $(boolof $val)
}

stringof ()
{
	echo "\"$1\""
}

boolof ()
{
	if [ -z "$1" ]
	then
		echo "false"
	elif [ $1 -ne 0 ]
	then
		echo "true"
	else
		echo "false"
	fi
}

notboolof ()
{
	if [ -z "$1" ]
	then
		echo "true"
	elif [ $1 -eq 0 ]
	then
		echo "true"
	else
		echo "false"
	fi
}

Help ()
{
	[ -n "$1" ] && echo "${progname}: unknown option \"$arg\""

	cat <<- _EOF_
	${progname} [opts] files...
	opts:
	  -h | --help               Show this help, then exit.
	  -c | --check              Check cert files for problems [current=$(boolof $doCheck)].
	  -a | --all                Show all info about each cert [current=$(CurDetail $OPT_ALL)].
	  -m | --most               Show most info about each cert [current=$(CurDetail $OPT_MOST)].
	  -s | --some               Show some info about each cert [current=$(CurDetail $OPT_SOME)].
	  -S | --summary            Show summary info about each cert [current=$(CurDetail $OPT_SUMMARY)].

	The "--all", "--most", "--some", and "--summary" options are ordered
	by the amount of detailed info they provide, from most to least.

	Options and files are processed left-to-right, so files are processed
	using only options to their left; thus later files may be processed
	with different options, if desired.

	If no file args are provided then certs are read from stdin.
	Similarly stdin is read when a file arg of "-" is processed.

	Any non-certificate lines in an input file are ignored,
	so ${progname} can be used as a filter in a pipe, e.g.:
	    openssl s_client -connect ibm.com:443 < /dev/null | x509-show-chain
	or even:
	    openssl s_client -connect ibm.com:443 < /dev/null \\
		        | x509-show-chain cert1.crt -c - -S cert2.crt

	${progname} exit status values:
	  0    No problems were detected.
	  1    Some files were not readable.
	  2    Problems were found within at least one certificate file.
	  3    Invalid parameters were given, or help was requested.

	  NOTE: If multiple problems were encountered then
	        the exit status will reflect only the last error.
_EOF_

	exit $EXIT_HELP
}

process_args ()
{
	local opts="$summaryopts"
	local first=1

	for arg in "$@"
	do
		case "$arg" in
		-h | --help)
			Help
			;;
		-c | --check)
			doCheck=1
			;;
		-a | --all)
			showDetails=$OPT_ALL
			opts=${options[$OPT_ALL]}
			;;
		-m | --most)
			showDetails=$OPT_MOST
			opts=${options[$OPT_MOST]}
			;;
		-s | --some)
			showDetails=$OPT_SOME
			opts=${options[$OPT_SOME]}
			;;
		-S | --summary)
			showDetails=$OPT_SUMMARY
			opts=${options[$OPT_SUMMARY]}
			;;
		-?*)
			Help "$arg"
			;;
		*)
			if [ $first -eq 0 ]
			then
				echo
			else
				first=0
			fi

			[ "$arg" = "-" ] && arg="/dev/stdin"
			echo "=== $arg ==="
			x509_show_chain "$arg" $showDetails $opts
		;;
		esac
	done

	# read stdin if no file args
	if [ $first -ne 0 ]
	then
		arg="/dev/stdin"
		echo "=== $arg ==="
		x509_show_chain "$arg" $showDetails $opts
	fi
}

process_args "$@"

exit $exitStatus