#!/bin/sh

# vim:ts=5:sw=5
VERSION="1.20a"

# Simple program for spotting weak SSL encryption (ciphers and version)
# Dirk Wetter (c) 2007, contact see http://drwetter.eu/contact.en.html
# License: GPLv2, see http://www.fsf.org/licensing/licenses/info/GPLv2.html
# and encompanying LICENSE FILE. Redistribution + modification under this
# license permitted. Usage w/o any warranty!
#
# If you enclose this script or modfications in your product, it has to
# be accompanied by the same license (see link) and the place where to get
# the recent version of this program: http://software.drwetter.eu/ssl/
# Please don't violate the license.


# Note that 56Bit ciphers are disabled during compile time in $OPENSSL > 0.9.8c
# (http://rt.$OPENSSL.org/Ticket/Display.html?user=guest&pass=guest&id=1461)
# ---> TLS1_ALLOW_EXPERIMENTAL_CIPHERSUITES in ssl/tls1.h . For testing it's recommended 
# to change this to 1 and recompile e.g. w/ ./config --prefix=/usr/  --openssldir=/etc/ssl
# (As a courtesy I provide openssl bins w/ 56Bit enabled for 32+64 bit Linux (see website)


OPENSSL="${OPENSSL:-/usr/bin/openssl}"	# your compiled private version or my supplied version
								# some place else --> edit this or edit $OPENSSL in $ENV
NC=""			# netcat will be autodetermined
ECHO="/bin/echo -e" # works under Linux, watch out under Solaris, not tested yet under cygwin 
COLOR=0			# with screen, tee and friends put 1 here (i.e. no color)
SHOWCIPHERS=yes     # determines whether the client side ciphers are displayed at all (makes no sense normally)
VERB_CLIST=""	     # ... and if so, "-v" shows them row by row cipher, SSL-version, KX, Au, Enc and Mac
VERBERR=0			# 0 means $OPENSSL errors will be more verbose
NOPARANOID=0		# 0 means it won't complain about medium ciphers
DEBUG=${DEBUG:-1}	# if 0 the temp file won't be erased. Currently only keeps the last output anyway


# some functions for text:
off() { 
	if [ $COLOR = 0 ]; then $ECHO "\033[0;30m "; fi
}

blue() { 
	if [ $COLOR = 0 ]; then $ECHO "\033[1;34m$1 "; else $ECHO "$1 "; fi
	off
}

red() { 
	if [ $COLOR = 0 ]; then $ECHO "\033[1;31m$1 "; else $ECHO "**$1** "; fi
	off
}
green() { 
	if [ $COLOR = 0 ]; then $ECHO "\033[1;32m$1 "; else $ECHO "$1 "; fi
	off
}
bold() {
	$ECHO "\033[1m$1"
	off
}


# whether it is ok for offer/not offer enc/cipher/version
ok(){
	# argv1: return value, argv2: would 0 be ok?
	if [ "$2" -eq 1 ] ; then		
		case $1 in
			1) red "offered (NOT ok)" ;;
			0) green "not offered (ok)" ;;
		esac
	else	
		case $1 in
			1) green "offered (ok)" ;;
			0) echo "not offered" ;;
		esac
	fi
	return $2
}


# test for versions #1: $OPENSSL arg#2: string to output, #3: is that good or bad?
testversion() {
	NEG_STR="handshake failure"
	$ECHO "$2: \c "
	echo | $OPENSSL  s_client $1 -connect "$NODE":"$PORT" 2>&1 | grep "$NEG_STR"
	ok $? $3
}

# list ciphers (and makes sure you have them locally configured)
# arg[1]: cipher class
listciphers() {
	if [ $VERBERR = 0 ]; then
		$OPENSSL ciphers "$VERB_CLIST" $1 2>&1 >$TMPFILE
	else
		$OPENSSL ciphers "$VERB_CLIST" $1  >&1 >$TMPFILE
	fi
	return $?
}


# argv[1]: cipherclass to test  (currently no single ciphers are tested)
# argv[2]: string on console
# argv[3]: ok to offer? 0: yes, 1: no
ciphers() {
	blue "$2: \c"; 
	if listciphers $1; then
		[ x$SHOWCIPHERS = "xyes" ] && $ECHO "local ciphers are: \c" && cat $TMPFILE | sed 's/:/, /g'
		if [ $VERBERR = 0 ]; then
			NEG_STR="handshake failure"
			echo | $OPENSSL  s_client -cipher $1  -connect "$NODE":"$PORT" 2>&1 | grep "$NEG_STR"
			ok $? "$3"
		else
			NEG_STR="Cipher is"
			echo | $OPENSSL  s_client -cipher $1  -connect "$NODE":"$PORT" >&1 >$TMPFILE
			if grep -q "$NEG_STR" $TMPFILE; then
				ok 1 "$3"
			else
				ok 0 "$3"
			fi
		fi
	else
		bold "Note: no remote test for $2 possible, local config problem"
		echo
	fi
}

# test for all ciphers locally configured (w/o distruguishing whether they
# are good or bad
allciphers(){
	NEG_STR="handshake failure"
	for c in `$OPENSSL  ciphers 'ALL:COMPLEMENTOFALL' | sed 's/:/ /g'`; do 
		$ECHO; bold "$c: \c " 
		if [ $VERBERR = 0 ]; then
			GREP="grep"
		else
			GREP="grep -q"
		fi
		echo | $OPENSSL  s_client -cipher $c -connect "$NODE":"$PORT" 2>&1 | $GREP "$NEG_STR"
		if [ "$?" -eq 0 ]; then	# grep is positive
			bold "nope"
		else
			bold "available"
		fi
	done
}

runtests() {
	echo
	testversion "-ssl3" "SSLv3" 0
	testversion "-tls1" "TLSv1" 0
	testversion "-ssl2" "SSLv2" 1

	ciphers NULL:eNULL "Null Cipher" 1
	ciphers aNULL "Anonymous DH Cipher " 1
	ciphers EXPORT40 "40 Bit encryption" 1
	ciphers EXPORT56 "56 Bit encryption" 1
	ciphers EXPORT "Export Cipher (general)" 1
	ciphers LOW "Low (<=64 Bit)" 1
	# to declare next one as bad is a matter of paranoia, see variable above:
	ciphers "MEDIUM:!NULL:!aNULL:!SSLv2" "SSLv3 but medium grade encryption" $NOPARANOID
	ciphers "HIGH:!NULL:!aNULL" "High grade encryption" 0
	return 0
}

renego() {
	# This tests for CVE-2009-3555 / RFC5746. Happy to receive feedback on this
	OSSL_VER=`$OPENSSL version | awk -F' ' '{ print $2 }'`
	case "$OSSL_VER" in
		0.9.7*|0.9.6*|0.9.5*)
			# 0.9.5a was latest in 0.9.5 an realeased 2000/4/1, that'll suffice for this test
			echo "your openssl version cannot test the secure renegotiation vulnerability"
			exit 3
			;;
		0.9.8*)
			minor=`echo "$OSSL_VER" | tr -d '[0-9.]'`
			case "$minor" in
				[a-l])
					echo "your openssl version cannot test the secure renegotiation vulnerability"
					exit 3
					;;
				[m-z])
					# all ok
					;;
			esac
		;;
		0.9.9*|1.0*)
			# all ok
		;;
	esac
	blue "Testing TLS/SSL Renegotiation Vulnerability\c"
	echo "(see also CVE 2009-3555, RFC5746, OSVDB: 59968-59974)"
	echo R | $OPENSSL s_client -connect  "$NODE":"$PORT" &>/dev/null
	reneg_ok=$?					# 0=client is renegotiating and does not gets an error: that should not be!
	NEG_STR="Secure Renegotiation IS NOT"
	echo R | $OPENSSL s_client -connect  "$NODE":"$PORT" 2>&1 | grep -iq "$NEG_STR"
	secreg=$?						# 0= Secure Renegotiation IS NOT supported

	if [ $reneg_ok -eq 0 ] && [ $secreg -eq 0 ]; then
		# Client side renegotiation is accepted and secure renegotiation IS NOT supported 
		red "is vulnerable (not ok)"
		return 1
	fi
	if [ $reneg_ok -eq 1 ] && [ $secreg -eq 1 ]; then
		green "not vulnerable (=ok)"
		return 0
	fi
	if [ $reneg_ok -eq 1 ] ; then   # 1,0
		bold "got an error from the server while renegotiating on client: should be ok ($reneg_ok,$secreg)"
		return 0
	fi
	bold "Patched Server detected ($reneg_ok,$secreg), probably ok"	# 0,1
	return 0
}

find_openssl_binary() {
	if [ ! -x "$OPENSSL" ]; then
		echo
		echo "cannot execute specified ($OPENSSL) openssl binary."
		$ECHO "trying to find one in \$PATH anyway...\c "
		OPENSSL=`which openssl`
		if [ "$?" -eq 0 ]; then
			echo "found @ $OPENSSL, continuing"
		else
			echo "sorry, bye."
			exit 1
		fi
	fi
	export OPENSSL
	return 0
}


find_nc_binary() {
	NC=`which -a netcat` 
	if [ "$?" -ne 0 ]; then
	 	NC=`which -a nc`
		if [ "$?" -ne 0 ]; then
			echo "sorry no netcat found, bye."
			exit 1
		fi
	fi
	return 0
}

help() {
	PRG=`basename $0`
	cat << EOF

$PRG <options> URI

where <options> is one of

	<-h|--help>:                 what you're looking at
	<-b|--banner>:               display banner + version
	<-v|--version>:              display banner + version
	<-a|--all>:                  check all ciphers individually
    	<-r|--renegotiation>:        tests only for renegotiation vulnerability

URI is 	hostname|hostname:port|URL|URL:port

        (port 443 is assumed unless otherwise specified)

EOF
	return $?
}


mybanner() {
	me=`basename $0`
	osslver=`$OPENSSL version`
	osslpath=`which $OPENSSL`
	cat <<EOF

############################################################
$me version $VERSION by Dirk Wetter

This program is free software. Redistribution + modification
under GPLv2 is permitted. Usage w/o any warranty.
############################################################

Using "$osslver" 
   in "$osslpath"

EOF
}

maketempf () {
	TMPFILE=`mktemp /tmp/ssltester.$NODE.XXXXXX` || exit 6
}

cleanup () {
	rm $LOGFILE 2>/dev/null
	if [ "$DEBUG" -eq 0 ] ; then
		echo $TMPFILE 
	else
		rm $TMPFILE
	fi
}

parse_hn_port() {
	PORT=443		# unless otherwise auto-determined, see below
	NODE="$1"

	# strip "https", supposed it was supplied additionally
	echo $NODE | grep -q 'https://' && NODE=`echo $NODE | sed 's/https\:\/\///'`

	# determine port, supposed it was supplied additionally
	echo $NODE | grep -q ':' && PORT=`echo $NODE | sed 's/^.*\://'` && NODE=`echo $NODE | sed 's/\:.*$//'`

	# check if netcat can connect to port
	if ! $NC -z -v -w 2  $NODE $PORT 2>/dev/null; then
		echo "Supply a host/port pair which works, "
		echo "@ $NODE:$PORT doesn't seem to be any server"
		echo
		exit 3
	fi

	# is ssl service listening on port?
	if ! echo | openssl s_client -connect $NODE:$PORT &>/dev/null; then
		echo "On port $PORT @ $NODE is no SSL server"
		exit 3
	fi
	tojour=`date +%F`" "`date +%R`
	echo
	bold "Testing now ($tojour): ---> $NODE:$PORT <---"
	echo
}



################# main: #################

find_openssl_binary

case "$1" in
	"" | \
	-h*|--help)
		help
		exit $?
		;;
     -b|--banner|-v|--version)
		mybanner
		exit $?
		;;
esac

# auto determine where bins are
find_nc_binary

mybanner

case "$1" in
	-a|--all)
		parse_hn_port "$2"
	   	maketempf
		allciphers 
		ret=$?
		cleanup 
		exit $ret
		;;
	-r|--renegotiation)
		parse_hn_port "$2"
		renego
		exit $?
		;;
	*)
		parse_hn_port "$1"
		maketempf
		renego
		ret=$?
		runtests
		let ret=$?+$ret
		cleanup 
		exit $ret
		;;
esac


