From c52e008347d84526444718732440887c716b91f9 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Thu, 6 Nov 2014 01:43:05 +0100 Subject: [PATCH] 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. --- cipherscan | 280 ++++++++++++++++++++++++++++++++++++++++- top1m/parse_results.py | 40 ++++++ top1m/testtop1m.sh | 4 +- 3 files changed, 321 insertions(+), 3 deletions(-) diff --git a/cipherscan b/cipherscan index 46bb981..409b349 100755 --- a/cipherscan +++ b/cipherscan @@ -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 diff --git a/top1m/parse_results.py b/top1m/parse_results.py index 0da5e42..8e69005 100644 --- a/top1m/parse_results.py +++ b/top1m/parse_results.py @@ -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): diff --git a/top1m/testtop1m.sh b/top1m/testtop1m.sh index 8a39055..faf8f26 100755 --- a/top1m/testtop1m.sh +++ b/top1m/testtop1m.sh @@ -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() {