add support for testing supported curves

since early versions of 1.0.2 openssl supports -curves command line
option, it allows us to set the curves advertised as supported

use the same approach to testing: advertise all, check what server
accepts, remove the accepted from list, repeat. When server aborts
connection or selects non ECC cipher, we know that we've tested all.
This commit is contained in:
Hubert Kario 2014-11-06 01:43:05 +01:00 committed by Julien Vehent
parent 800eff19ce
commit c52e008347
3 changed files with 321 additions and 3 deletions

View File

@ -61,6 +61,7 @@ TIMEOUT=30
# trust anchors are stored
CAPATH=""
SAVECRT=""
TEST_CURVES="False"
unset known_certs
declare -A known_certs
unset cert_checksums
@ -99,6 +100,7 @@ Use one of the options below:
-j | --json Output results in JSON format.
-o | --openssl path/to/your/openssl binary you want to use.
--savecrt path where to save untrusted and leaf certificates
--curves test ECC curves supported by server (req. OpenSSL 1.0.2)
-v | --verbose Increase verbosity.
The rest of the arguments will be interpreted as openssl s_client argument.
@ -529,6 +531,25 @@ display_results_in_terminal() {
else
echo "Client side cipher ordering"
fi
if [[ $TEST_CURVES == "True" ]]; then
if [[ -n $supported_curves ]]; then
echo
local i=0
(echo "prio curve"
for curve in ${supported_curves//,/ }; do
i=$((i+1))
echo "$i $curve"
done) | column -t
echo
if [[ $fallback_supported == True ]]; then
echo "Server does fallback on unsupported curves"
else
echo "Server doesn't fallback on unsupported curves"
fi
echo "$curves_ordering side curve ordering"
fi
fi
}
display_results_in_json() {
@ -553,7 +574,14 @@ display_results_in_json() {
echo -n "\"pfs\":\"$pfs\"}"
ctr=$((ctr+1))
done
echo ']}'
echo -n ']'
if [[ -n $supported_curves ]]; then
echo -n ","
echo -n "\"curve_fallback\":\"$fallback_supported\","
echo -n "\"curve_ordering\":\"$curves_ordering\","
echo -n "\"curve\":[\"${supported_curves//,/\",\"}\"]"
fi
echo '}'
}
test_serverside_ordering() {
@ -609,6 +637,248 @@ test_serverside_ordering() {
fi
}
test_ecc_curves() {
# openssl formated list of curves that will cause server to select ECC suite
local ecc_ciphers=""
# names of all curves supported in TLS (as supported by openssl)
local curves=()
# alternative names for curves in TLS (as returned by openssl s_client or
# as specified in standard)
local curve_names=()
# "True" if server supports ciphers that don't use ECC at a lower priority
local fallback_available="False"
# return variable: list of curves supported by server, in order
supported_curves=""
# return variable: whatever a server will fall back to non ECC suite when
# client doesn't advertise support for curves the server needs
fallback_supported="False"
# return variable: check if server uses server side or client side ordering
# for curves
curves_ordering="server"
# get ciphers that will cause server to select suite that uses ECC, if most
# preferred ciphers do not, exclude them from list
for pref in "${cipherspref[@]}"; do
# get first value from space separated array
cipher=($pref)
# ECDH uses curve from certificate, so no way to negotiate it, the two
# below are ephemeral so they can select the curve independent of cert
if [[ $cipher =~ ECDHE ]] || [[ $cipher =~ AECDH ]]; then
# colon on end of string is ignored by openssl
ecc_ciphers+="${cipher}:"
elif [[ -n $ecc_ciphers ]]; then
# add fallback ciphers
ecc_ciphers+="${cipher}:"
fallback_available="True"
fi
done
if [[ -z $ecc_ciphers ]]; then
verbose "Server does not support ephemeral ECC"
return
fi
verbose "ECC curves testing, cipher preference: $ecc_ciphers"
# only some curves have defined TLS code points, so no use parsing openssl
# output checking what it supports, see
# http://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-8
curves=(sect163k1 # K-163
sect163r1
sect163r2 # B-163
sect193r1
sect193r2
sect233k1 # K-233
sect233r1 # B-233
sect239k1
sect283k1 # K-283
sect283r1 # B-283
sect409k1 # K-409
sect409r1 # B-409
sect571k1 # K-571
sect571r1 # B-571
secp160k1
secp160r1
secp160r2
secp192k1
prime192v1 # P-192 secp192r1
secp224k1
secp224r1 # P-224
secp256k1
prime256v1 # P-256 secp256r1
secp384r1 # P-384
secp521r1 # P-521
brainpoolP256r1
brainpoolP384r1
brainpoolP512r1)
# many curves have alternative names, save those too
curve_names=("sect163k1 K-163"
"sect163r1"
"sect163r2 B-163"
"sect193r1"
"sect193r2"
"sect233k1 K-233"
"sect233r1 B-233"
"sect239k1"
"sect283k1 K-283"
"sect283r1 B-283"
"sect409k1 K-409"
"sect409r1 B-409"
"sect571k1 K-571"
"sect571r1 B-571"
"secp160k1"
"secp160r1"
"secp160r2"
"secp192k1"
"prime192v1 P-192 secp192r1"
"secp224k1"
"secp224r1 P-224"
"secp256k1"
"prime256v1 P-256 secp256r1"
"secp384r1 P-384"
"secp521r1 P-521"
"brainpoolP256r1"
"brainpoolP384r1"
"brainpoolP512r1")
OLDIFS="$IFS"
IFS=':'
verbose "Will test following curves: ${curves[*]}"
IFS="$OLDIFS"
# prepare the ssl command we'll be using
local sslcommand=""
sslcommand="$TIMEOUTBIN $TIMEOUT $OPENSSLBIN s_client"
if [ -n "$CAPATH" ]; then
sslcommand+=" -CApath $CAPATH -showcerts"
elif [ -e "$CACERTS" ]; then
sslcommand+=" -CAfile $CACERTS"
fi
sslcommand+=" -status $SCLIENTARGS -connect $TARGET -cipher $ecc_ciphers"
#
# here we use the same logic as with detecting cipher suites: first
# advertise all curves as supported, then remove curves one by one until we
# either get a fallback to a non ECC cipher, we run of curves or server
# tries to negotiate a curve we didn't advertise
#
while [[ ${#curves[@]} -gt 0 ]]; do
OLDIFS="$IFS"
IFS=':'
local test_curves="${curves[*]}"
IFS="$OLDIFS"
verbose "Testing $test_curves"
ratelimit
local tmp=$(echo Q | $sslcommand -curves $test_curves 2>/dev/null)
parse_openssl_output <<<"$tmp"
if [[ -z $current_protocol || $current_cipher == "(NONE)" \
|| $current_cipher == '0000' ]]; then
# server aborted connection
if [[ $fallback_available == "True" ]]; then
fallback_supported="False"
else
fallback_supported="unknown"
fi
break
else
# server accepted connection
local ephem_data=(${current_pfs//,/ })
if [[ ${ephem_data[0]} =~ ECDH ]]; then
# ok, we got an ECC connection, now, what curve did we get
for id in "${!curves[@]}"; do
# compare to alternative names
if [[ ${curve_names[$id]} =~ ${ephem_data[1]} ]]; then
if [[ -n $supported_curves ]]; then
supported_curves+=","
fi
supported_curves+="${curves[$id]}"
verbose "Server selected ${ephem_data[1]}, a.k.a ${curves[$id]}"
# ok, we know it's supported, remove it from set of offered ones
unset curves[$id]
break
fi
done
else
verbose "Server fell back to $current_cipher"
# ok, we got a fallback
fallback_supported="True"
break
fi
fi
[ "$OUTPUTFORMAT" == "terminal" ] && [ $DEBUG -lt 1 ] && echo -n '.'
done
# don't penalize servers that will negotiate all curves we know of...
if [[ ${#curves[@]} -eq 0 ]]; then
fallback_supported="unknown"
fi
#
# check if curves ordering is server of client side
#
local tmp_curves=(${supported_curves//,/ })
verbose "Server supported curves: ${tmp_curves[@]}"
# server supports just one or none, so it effectively uses server side
# ordering (as it dictates what curves client must support)
if [ ${#tmp_curves[@]} -lt 2 ]; then
curves_ordering="server"
else
# server supports at least 2 curves, rotate their order, see if
# selected changes
test_curves=""
most_wanted="${tmp_curves[${#tmp_curves[@]}-1]}"
for (( i=${#tmp_curves[@]}-1; i>0; i--)); do
test_curves+="${tmp_curves[$i]}:"
done
test_curves+="${tmp_curves[0]}"
verbose "Testing ordering with $sslcommand -curves $test_curves"
ratelimit
local tmp=$(echo Q | $sslcommand -curves $test_curves 2>/dev/null)
parse_openssl_output <<<"$tmp"
if [[ -z $current_protocol || $current_cipher == "(NONE)" \
|| $current_cipher == '0000' ]]; then
fallback_supported="order-specific"
verbose "Server aborted connection"
else
local ephem_data=(${current_pfs//,/ })
verbose "Server selected $current_cipher with $current_pfs"
verbose "ephem_data: ${ephem_data[@]}"
if [[ ${ephem_data[0]} =~ ECDH ]]; then
verbose "Server did select ${ephem_data[1]} curve"
curves_ordering="inconclusive-${ephem_data[1]}"
for id in "${!curve_names[@]}"; do
if [[ ${curve_names[$id]} =~ ${ephem_data[1]} ]]; then
local canonic_name=(${curve_names[$id]})
if [[ ${canonic_name[0]} == $most_wanted ]]; then
curves_ordering="client"
break
else
curves_ordering="server"
break
fi
fi
done
else
# some servers downgrade to non ECDH when curve order is changed
curves_ordering="inconclusive-noecc"
fi
fi
fi
}
# If no options are given, give usage information and exit (with error code)
if [ $# -eq 0 ]; then
usage;
@ -664,6 +934,10 @@ do
SAVECRT="$2"
shift 2
;;
--curves)
TEST_CURVES="True"
shift 1
;;
--) # End of all options
shift
break
@ -723,6 +997,10 @@ get_cipher_pref $CIPHERSUITE
test_serverside_ordering
if [[ $TEST_CURVES == "True" ]]; then
test_ecc_curves
fi
if [ "$OUTPUTFORMAT" == "json" ]; then
display_results_in_json
else

View File

@ -72,6 +72,9 @@ handshakestats = defaultdict(int)
keysize = defaultdict(int)
sigalg = defaultdict(int)
tickethint = defaultdict(int)
eccfallback = defaultdict(int)
eccordering = defaultdict(int)
ecccurve = defaultdict(int)
ocspstaple = defaultdict(int)
dsarsastack = 0
total = 0
@ -86,6 +89,9 @@ for r,d,flist in os.walk(path):
tempdsakeystats = {}
tempsigstats = {}
tempticketstats = {}
tempeccfallback = "unknown"
tempeccordering = "unknown"
tempecccurve = {}
""" supported ciphers by the server under scan """
tempcipherstats = {}
ciphertypes = 0
@ -141,6 +147,17 @@ for r,d,flist in os.walk(path):
if len(results['ciphersuite']) < 1:
continue
""" save ECC curve stats """
if 'curve_fallback' in results:
tempeccfallback = results['curve_fallback']
if 'curve_ordering' in results:
tempeccordering = results['curve_ordering']
if 'curve' in results:
for curve in results['curve']:
tempecccurve[curve] = 1
if len(results['curve']) == 1:
tempecccurve[curve + ' Only'] = 1
""" loop over list of ciphers """
for entry in results['ciphersuite']:
@ -329,6 +346,11 @@ for r,d,flist in os.walk(path):
for s in tempticketstats:
tickethint[s] += 1
eccfallback[tempeccfallback] += 1
eccordering[tempeccordering] += 1
for s in tempecccurve:
ecccurve[s] += 1
if ocsp_stapling is None:
ocspstaple['Unknown'] += 1
elif ocsp_stapling:
@ -518,6 +540,24 @@ for stat in sorted(pfsstats):
pfspercent = round(pfsstats[stat] / handshakestats['DHE'] * 100, 4)
sys.stdout.write(stat.ljust(25) + " " + str(pfsstats[stat]).ljust(10) + str(percent).ljust(9) + str(pfspercent) + "\n")
print("\nSupported ECC curves Count Percent ")
print("-------------------------+---------+--------")
for stat in sorted(ecccurve):
percent = round(ecccurve[stat] / total * 100, 4)
sys.stdout.write(stat.ljust(25) + " " + str(ecccurve[stat]).ljust(10) + str(percent).ljust(9) + "\n")
print("\nUnsupported curve fallback Count Percent ")
print("------------------------------+---------+--------")
for stat in sorted(eccfallback):
percent = round(eccfallback[stat] / total * 100,4)
sys.stdout.write(stat.ljust(30) + " " + str(eccfallback[stat]).ljust(10) + str(percent).ljust(9) + "\n")
print("\nECC curve ordering Count Percent ")
print("-------------------------+---------+--------")
for stat in sorted(eccordering):
percent = round(eccordering[stat] / total * 100, 4)
sys.stdout.write(stat.ljust(25) + " " + str(eccordering[stat]).ljust(10) + str(percent).ljust(9) + "\n")
print("\nTLS session ticket hint Count Percent ")
print("-------------------------+---------+--------")
for stat in natural_sort(tickethint):

View File

@ -68,7 +68,7 @@ function scan_host() {
if [ $? -gt 0 ]; then
return
fi
../cipherscan --capath ca_files --saveca --savecrt certs --delay 2 -json -servername $1 $2:443 > results/$1@$2
../cipherscan --capath ca_files --saveca --curves --savecrt certs --delay 2 -json -servername $1 $2:443 > results/$1@$2
}
function scan_host_no_sni() {
@ -80,7 +80,7 @@ function scan_host_no_sni() {
if [ $? -gt 0 ]; then
return
fi
../cipherscan --capath ca_files --saveca --savecrt certs --delay 2 -json $1:443 > results/$1
../cipherscan --capath ca_files --saveca --curves --savecrt certs --delay 2 -json $1:443 > results/$1
}
function scan_hostname() {