#!/usr/bin/bash
#
# Discovery Usage: $0 [--discover [prefix]]
# Lookup Usage: $0 <addr> (startdate|enddate|serial|subject|issuer|dhlen|sigalgo|carootsubject|trustanchorissuer|trustanchorsubject|verify|chainlength|fingerprint) [SNI-servername]
#

# List of extra targets in the form [<SNI>@]<ADDR>
EXTRATARGETS=/etc/zabbix/noggin/net-certificate-targets

function usage  {
	cat <<-EOD >&2

		TLS network service discovery

		    $0 --discover [<prefix>]

		TLS propery checks

		    $0 <address> <property> [<SNI-name>]

		    Where <property> can be one of:

		      * startdate          - The service certificate's notBefore date as unix epoch seconds
		      * enddate            - The service certificate's notAfter date as unix epoch seconds
		      * serial             - The service certificate's serial number
		      * subject            - The service certificate's subject in the form /A=B/D=C/ (eg /C=AU/ST=...)
		      * fingerprint        - The service certificate's fingerprint
		      * issuer             - The service certificate's issuer in the form /A=B/D=C/ (eg /C=AU/ST=...)
		      * dhlen              - The diffie-hellman (DH) key length presented by the service
		      * ecdhlen            - The elliptic-curve diffie-hellman (ECDHE) key length presented by the service
		      * sigalgo            - The service certificate's signature algorithm (eg md5WithRSAEncryption, sha1With.., sha256With... etc)
		      * minalgo            - The "worst" certificate signature algorithm found in the chain (eg sha256 cert + sha1 intermediate => sha1)
		      * carootsubject      - The subject of the certificate in the chain where issuer==subject (may be empty)
		      * trustpath          - The subject of all certificates used in the attempted verification of this service (including chain and local)
		      * trustanchorsubject - The issuer of the 'last' (ie closest to the root) certificate in the chain, that is the expected subject of the system trust anchor
		      * time-to-expiry     - The number of seconds until the enddate (or 0 if already expired or not yet valid)
		      * verify             - Whether the local server would trust the service (openssl verify return code)
		      * chainlength        - How many certificates are in the chain as served by the service (including the service cert)
		      * chain              - The subjects of the the certificates served in the chain

		Notes & Limitations

		    * Only relatively well known network ports are scanned

		    * Only plain TLS services and FTP, SMTP, POP3 and IMAP StartTLS services are discovered
		      specifically mysql and radius (EAP) certificates are NOT discovered at this time

		    * SNI names are only discovered for HTTPS services (by fetching the running config)


	EOD
	exit 1
}

# Optional discovery key prefix
if [ "$1" == "--discover" ]; then
  shift
  shift
  PREFIX="$2"
fi

function protoflags {
  local PROTO=
  case $1 in
     *:21) PROTO=ftp;;
     *:25) PROTO=smtp;;
    *:587) PROTO=smtp;;
    *:110) PROTO=pop3;;
    *:143) PROTO=imap;;
  esac
  echo ${PROTO:+-starttls $PROTO}
}

function getcert {
	local ADDR=$1
	local NAME=$2

	case ${ADDR} in
		*:1812)
			"$(dirname "$(readlink -f "$0")")/net-certificate-radeap" "${ADDR}" "${NAME}"
			;;

		*:3306) ;&
		*:3307)
			"$(dirname "$(readlink -f "$0")")/net-certificate-mysql" "${ADDR}"
			;;

		*)
		    # shellcheck disable=SC2181,SC2046
			openssl s_client -connect "${ADDR}" $(protoflags "${ADDR}") ${NAME:+-servername "$NAME"} </dev/null
			;;
	esac
}

# Do we have support for SNI in our client?
if openssl s_client -help 2>&1 | grep -q -- '-servername'; then
	HAVESNI=1
else
	HAVESNI=0
fi


if [ $# -gt 0 ]; then

	ADDR=$1
	PARAM=$2
	if [ $HAVESNI -eq 1 ] && [ "$SNI" != "0" ]; then
                if [ "$3" != 'DEFAULT' ]; then
			NAME="$3"
			SNI=${3:+-servername $3}
		else
			NAME=
			SNI=
		fi
	else
		NAME=
		SNI=
	fi

	STARTTLS=$(protoflags "$ADDR")

    case $PARAM in
		dhlen) # DH Key length (force kEDH ciphers only)
            # shellcheck disable=SC2086
			openssl s_client -cipher 'kEDH' -connect "${ADDR}" $STARTTLS $SNI </dev/null 2>&1 \
			  | awk '/Server Temp Key: [^ ]+ [0-9]+ bits/{print $5}; / alert handshake failure/{print "ZBX_NOTSUPPORTED"};'
			;;

		ecdhlen) # ECDHE Key length (force kECDHE ciphers only)
		    # shellcheck disable=SC2086
			openssl s_client -cipher 'kECDHE' -connect "${ADDR}" $STARTTLS $SNI </dev/null 2>&1 \
			| awk '/Server Temp Key: [^ ]+ [0-9]+ bits/{print $5}; / alert handshake failure/{print "ZBX_NOTSUPPORTED"};'
			;;

		keylen) # (RSA) server public key length
			getcert "${ADDR}" "${NAME}" </dev/null 2>&1 \
			  | openssl x509 -noout -text \
			  | sed -nre 's/^[ \t]+Public-Key: \(([0-9]+) bit\)[ \t]*$/\1/  p'
			  #| awk '/Server public key is [0-9]+ bit/{print $5}; / alert handshake failure/{print "ZBX_NOTSUPPORTED"};'
			;;

		sigalgo) # Certificate signature algorithm
			getcert "${ADDR}" "${NAME}"  </dev/null 2>/dev/null \
			  | sed -nre '/BEGIN CERTIFICATE/,/END CERTIFICATE/ p' \
			  | openssl x509 -noout -text \
			  | awk '/Signature Algorithm: /{print $3; exit;}'
			;;

		minalgo) # 'worst' signature algorithm in the presented chain currently only tested with (md5|sha1|sha256)WithRSAEncryption
			# shellcheck disable=2086
			openssl s_client -showcerts -connect "${ADDR}" $STARTTLS $SNI </dev/null 2>/dev/null \
			  | sed -nre '/BEGIN CERTIFICATE/,/END CERTIFICATE/ p' \
			  | openssl crl2pkcs7 -nocrl -certfile /dev/stdin 2>/dev/null \
			  | openssl pkcs7 -print_certs -text 2>/dev/null \
			  | awk '/^[ \t]+Signature Algorithm: /{print $3}' \
			  | sort \
			  | head -1
			  ;;

		time-to-expiry) # Seconds remaining until the enddate (or 0 for outside validity window)
			getcert "${ADDR}" "${NAME}"  </dev/null 2>/dev/null \
			    | sed -nre '/BEGIN CERTIFICATE/,/END CERTIFICATE/ p' \
			    | openssl x509 -noout -dates \
			    | perl -MDate::Parse -ne '
			        s/^.*?= *//g; push(@ts, str2time($_));
			        END {
			            my $now=time();
			            printf("%s\n", (($now < $ts[0] || $now > $ts[1]) ? 0 : ($ts[1] - $now)))
			        }
			      '
			;;

		carootsubject) # Subject of the root (trust anchor) certificate
            # shellcheck disable=2086
			openssl s_client -connect "${ADDR}" $STARTTLS $SNI 2>&1 </dev/null \
                          | awk -vSUBJ= -vOK=0 '
				/^depth=[0-9]+ (.*)$/ && SUBJ == "" {
					gsub("^depth=[0-9]+ ", "");
					gsub(" = ", "=");
					gsub(", ", "/");
					SUBJ="/" $0
				};
				/^[ \t]*[Vv]erify return code: 0 [(]ok[)]$/{
				        OK=1
                                        exit
				};
				END {
				    if (OK == 0 || SUBJ == "") {
				        print "Unknown"
                                    } else {
                                        print SUBJ;
                                    }
                                }
			    '
			;;

		trustanchorsubject) # Subject of the trust anchor certificate (not necessarily the actual root)
            # shellcheck disable=2086
			openssl s_client -connect "${ADDR}" $STARTTLS $SNI 2>&1 </dev/null \
			  | sed -nre 's/[ \t]+i:(\/.*)$/\1/ p' | tail -1
			;;

		trustpath) # Subject names of all certificates in the trustpath / chain (only valid when verify = 0 (ok)!
            # shellcheck disable=2086
			openssl s_client -CAfile /etc/pki/tls/certs/ca-bundle.crt -CApath /etc/pki/tls/certs/ -verify 10 -connect "${ADDR}" $STARTTLS $SNI </dev/null 2>&1  \
			  | sed -nre 's/^depth=[0-9]+[ \t]+(.*)/\/\1/p' | sed -re 's/ = /=/g; s/, /\//g;'
			;;

		chain) # Length of the chain provided by the server (excluding the end entity)
            # shellcheck disable=2086
			openssl s_client -showcerts -connect "${ADDR}" $STARTTLS $SNI </dev/null 2>&1 \
			  | sed -nre '/^Certificate chain$/,/^--+/ p' \
                          | sed -nre 's/^[ \t]+i:(.*)$/\1/g p'
			;;

		chainlength) # Length of the chain provided by the server (excluding the end entity)
            # shellcheck disable=2086
			openssl s_client -showcerts -connect "${ADDR}" $STARTTLS $SNI </dev/null 2>&1 \
			  | awk -vD=0 '/^ +[0-9] s:/{D=$1};END{print D}'
			;;

		verify) # Verification result
            # shellcheck disable=2086
			openssl s_client -CAfile /etc/pki/tls/certs/ca-bundle.crt -CApath /etc/pki/tls/certs/ -verify 10 -connect "${ADDR}" $STARTTLS $SNI </dev/null 2>/dev/null \
		          | sort -u \
			  | sed -nre 's/Verify return code: (.*)$/\1/p'
			;;

		*)
			# "Direct" options that just pass through to openssl x509 subcommand
			if [[ ! "$PARAM" =~ ^((start|end)date|serial|subject|issuer|fingerprint)$ ]]; then
				usage
			fi

			#RESULT=$(openssl s_client -connect "${ADDR}" $STARTTLS $SNI 2>/dev/null </dev/null \
			RESULT=$(getcert "${ADDR}" "${NAME}" 2>/dev/null \
			  | sed -nre '/BEGIN CERTIFICATE/,/END CERTIFICATE/ p' \
			  | openssl x509 -noout "-$PARAM" 2>/dev/null \
			  | cut -d= -f2-)

			if [[ "$PARAM" =~ ^(start|end)date$ ]]; then
				RESULT="$(date --date="$RESULT" +%s)"
			fi
			echo "$RESULT"
			;;
	esac

else

	#
	# Zabbix discovery for SSL certificates used on network services
	#

	#
	# First try to discover apache certificates so we can use SNI names too
	#
	SNINAMES=()
	if [ $HAVESNI -eq 1 ]; then
		SNICERTS=$(
			env http_proxy= curl --silent http://127.0.0.1:280/_server/info\?mod_ssl.c 2>/dev/null \
				| sed -n -re 's/^.*SSLCertificateFile <i>([^<]+).*$/\1/g p' \
				| sort -u
		)
		BADCERTS=()
		if  pushd /etc/httpd >/dev/null 2>/dev/null; then
			for i in $SNICERTS; do
				i="$(readlink -f "${i}")"
				SNINAME=
				if [ -f "$i" ] && [ -r "$i" ]; then
                                        SNINAME="$(openssl x509 -in "$i" -subject 2>/dev/null |sed -nre 's|^.* CN = ([^/]+).*$|\1|gp' | sed -re 's/^\*\./STAR./g')"
				fi
				if [ -n "$SNINAME" ]; then
					SNINAMES+=( "$SNINAME" )
				else
					BADCERTS+=( "$i" )
				fi
			done
			popd >/dev/null 2>/dev/null || exit 1
        fi
	fi

	echo "{ \"data\":["
	cat <(
		# List unreadable certs which can cause SNI discovery to fail
		i=0
		if [ "${#BADCERTS[@]}" -gt 0 ]; then
			for FILE in "${BADCERTS[@]}"; do
				printf ' { "{#ADDR}":"0.0.0.0:0", "{#FINGERPRINT}":"BA:DC:F0:%02d:BA:DC:F0:%02d:BA:DC:F0:%02d:BA:DC:F0:%02d:BA:DC:F0:%02d", "{#CN}":"SSLCertificateFile.error", "{#SNI}":"%s" },\n' \
					$i $i $i $i $i "$FILE"
				((i++))
			done
		fi
	) <(
		for ADDR in $(netstat --listening --tcp -n | awk '$4 ~ /:(21|25|110|143|443|465|587|636|993|995|3307|8443)$/{print $4}' | sed -re 's/^:::([0-9]+)$/0.0.0.0:\1/g' | sort); do
			# STARTTLS?
			STARTTLS=$(protoflags "$ADDR")
			for SNI in DEFAULT "${SNINAMES[@]}"; do
				SNIFLAG=${SNI:+-servername $SNI}
				if [ $HAVESNI -eq 0 ] || [[ ! "$ADDR" =~ 443$ ]]; then
					SNIFLAG=
				fi
				echo -n " { \"{#ADDR}\":\"$ADDR\", "
				# shellcheck disable=SC2086
				getcert "${ADDR}" ${SNI} 2>/dev/null \
					| openssl x509 -noout -fingerprint -subject 2>/dev/null \
                                        | sed -re 's/^.+Fingerprint=(.*)$/\"{#FINGERPRINT}\":\"\1\", /g' -e 's/^subject=.*CN = ([^/]+).*/\"{#CN}\":\"\1\"/g' \
					| cat - <(echo ", \"{#SNI}\":\"$SNI\" },") \
					| tr -d '\n'
				echo
				[ -n "$SNIFLAG" ] || break;
			done
		done
		if [ -f ${EXTRATARGETS} ]; then
            # TODO: Fix this one properly
            # shellcheck disable=SC2013
			for TARGET in $(grep -Pv '^\s*#' ${EXTRATARGETS} | grep -Po '\S+') ; do
				ADDR="${TARGET##*@}"
				STARTTLS=$(protoflags "$ADDR")
				SNI='DEFAULT'
				SNIFLAG=
				if [ "${ADDR}" != "${TARGET}" ]; then
                                        SNI="${TARGET%@*}"
					SNIFLAG="-servername ${SNI}"
				fi
				echo -n " { \"{#ADDR}\":\"$ADDR\", "
				# shellcheck disable=2086
				openssl s_client -connect "${ADDR}:443" $SNIFLAG $STARTTLS </dev/null 2>/dev/null \
				        | openssl x509 -noout -fingerprint -subject 2>/dev/null \
                                        | sed -re 's/^.+Fingerprint=(.*)$/\"{#FINGERPRINT}\":\"\1\", /g' -e 's/^subject=.*CN = ([^/,]+)/\"{#CN}\":\"\1\"/g' \
				        | cat - <(echo ", \"{#SNI}\":\"$SNI\" },") \
				        | tr -d '\n'
				echo
			done
		fi
	) | grep FINGERPRINT | sed -re 's/:"DEFAULT"/:".0000-DEFAULT"/g' \
	      | sort | sort -k 2,3 -u | sed -re 's/:".0000-DEFAULT"/:"DEFAULT"/g' \
	      | sed -re '$ s/,$//g' -e "s/\"\\{#/\"\\{#${PREFIX}/g"
	echo "]}"

fi
