diff --git a/cipherscan b/cipherscan index 7da1be5..985eb07 100755 --- a/cipherscan +++ b/cipherscan @@ -68,6 +68,10 @@ TIMEOUT=30 # trust anchors are stored CAPATH="" SAVECRT="" +TEST_CURVES="False" +has_curves="False" +# openssl formated list of curves that will cause server to select ECC suite +ecc_ciphers="" unset known_certs declare -A known_certs unset cert_checksums @@ -106,6 +110,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 +--[no-]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. @@ -128,6 +133,68 @@ debug(){ fi } +# obtain an array of curves supported by openssl +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, this array provides a mapping to find the IANA +# name of a curve using its alias +CURVES_MAP=("sect163k1 K-163" + "sect163r2 B-163" + "sect233k1 K-233" + "sect233r1 B-233" + "sect283k1 K-283" + "sect283r1 B-283" + "sect409k1 K-409" + "sect409r1 B-409" + "sect571k1 K-571" + "sect571r1 B-571" + "prime192v1 P-192 secp192r1" + "secp224r1 P-224" + "prime256v1 P-256 secp256r1" + "secp384r1 P-384" + "secp521r1 P-521") + +get_curve_name() { + local identifier=$1 + for c in "${CURVES_MAP[@]}"; do + if [[ "$c" =~ $identifier ]]; then + verbose "$c matches identifier $identifier" + local map=(${c// / }) + echo ${map[0]} + return + fi + done + echo $identifier + return +} + c_hash() { local h=$(${OPENSSLBIN} x509 -hash -noout -in "$1/$2" 2>/dev/null) for ((num=0; num<=100; num++)) ; do @@ -371,6 +438,7 @@ test_cipher_on_target() { fi cipher=$current_cipher pfs=$current_pfs + [ -z $pfs ] && pfs="None" pubkey=$current_pubkey sigalg=$current_sigalg trusted=$current_trusted @@ -388,13 +456,28 @@ test_cipher_on_target() { # if cipher contains NONE, the cipher wasn't accepted elif [ "$cipher" == '(NONE) ' ]; then - result="$cipher $protocols $pubkey $sigalg $trusted $tickethint $ocspstaple $pfs" + result="$cipher $protocols $pubkey $sigalg $trusted $tickethint $ocspstaple $pfs $current_curves $curves_ordering" verbose "handshake failed, server returned ciphersuite '$result'" return 1 # the connection succeeded else - result="$cipher $protocols $pubkey $sigalg $trusted $tickethint $ocspstaple $pfs" + current_curves="None" + # if pfs uses ECDH, test supported curves + if [[ $pfs =~ ECDH ]]; then + has_curves="True" + if [ $TEST_CURVES == "True" ]; then + test_curves + if [ "$ecc_ciphers" != "" ]; then + ecc_ciphers+=":" + fi + ecc_ciphers+="$cipher" + else + # resolve the openssl curve to the proper IANA name + current_curves="$(get_curve_name $(echo $pfs|cut -d ',' -f2))" + fi + fi + result="$cipher $protocols $pubkey $sigalg $trusted $tickethint $ocspstaple $pfs $current_curves $curves_ordering" verbose "handshake succeeded, server returned ciphersuite '$result'" return 0 fi @@ -455,6 +538,7 @@ display_results_in_terminal() { local trusted local tickethint local ocspstaple + local curvesordering local different=False echo "Target: $TARGET"; echo for cipher in "${cipherspref[@]}"; do @@ -473,6 +557,9 @@ display_results_in_terminal() { trusted="${cipher_data[4]}" tickethint="${cipher_data[5]}" ocspstaple="${cipher_data[6]}" + if [[ $TEST_CURVES == "True" && "${cipher_data[9]}" != "" ]]; then + curvesordering="${cipher_data[9]}" + fi else if [ "$pubkey" != "${cipher_data[2]}" ]; then different=True @@ -486,23 +573,33 @@ display_results_in_terminal() { if [ "$tickethint" != "${cipher_data[5]}" ]; then different=True fi + if [ "$ocspstaple" != "${cipher_data[6]}" ]; then + different=True + fi + if [[ "$curvesordering" == "" && "${cipher_data[9]}" != "" ]]; then + curvesordering="${cipher_data[9]}" + fi + if [[ "$curvesordering" != "" && "$curvesordering" != "${cipher_data[9]}" ]]; then + different=True + fi fi results=("${results[@]}" "$r") ctr=$((ctr+1)) done + header="prio ciphersuite protocols" + if [ $different == "True" ]; then + header+=" pubkey_size signature_algoritm trusted ticket_hint ocsp_staple" + fi + header+=" pfs" + if [ $has_curves == "True" ]; then + header+=" curves" + if [[ $TEST_CURVES == "True" && $different == "True" ]]; then + header+=" curves_ordering" + fi + fi if [ $DOBENCHMARK -eq 1 ]; then - if [ $different == "True" ]; then - header="prio ciphersuite protocols pubkey_size signature_algoritm trusted ticket_hint ocsp_staple pfs_keysize avg_handshake_microsec" - else - header="prio ciphersuite protocols pfs_keysize avg_handshake_microsec" - fi - else - if [ $different == "True" ]; then - header="prio ciphersuite protocols pubkey_size signature_algorithm trusted ticket_hint ocsp_staple pfs_keysize" - else - header="prio ciphersuite protocols pfs_keysize" - fi + header+=" avg_handshake_microsec" fi ctr=0 for result in "${results[@]}"; do @@ -513,8 +610,8 @@ display_results_in_terminal() { if [ $different == "True" ]; then echo $result|grep -v '(NONE)' else - # prints priority, ciphersuite, protocols and pfs_keysize - awk '!/(NONE)/{print $1 " " $2 " " $3 " " $9}' <<<"$result" + # prints priority, ciphersuite, protocols and pfs + awk '!/(NONE)/{print $1 " " $2 " " $3 " " $9 " " $10}' <<<"$result" fi done|column -t echo @@ -532,9 +629,13 @@ display_results_in_terminal() { echo "OCSP stapling: not supported" fi if [[ $serverside == "True" ]]; then - echo "Server side cipher ordering" + echo "Cipher ordering: server" else - echo "Client side cipher ordering" + echo "Cipher ordering: client" + fi + if [ $TEST_CURVES == "True" ]; then + echo "Curves ordering: $curvesordering" + echo "Curves fallback: $fallback_supported" fi } @@ -557,10 +658,23 @@ display_results_in_json() { echo -n "\"ocsp_stapling\":\"${cipher_arr[6]}\"," pfs="${cipher_arr[7]}" [ "$pfs" == "" ] && pfs="None" - echo -n "\"pfs\":\"$pfs\"}" + echo -n "\"pfs\":\"$pfs\"" + if [[ "${cipher_arr[0]}" =~ ECDH ]]; then + echo -n "," + echo -n "\"curves\":[\"${cipher_arr[8]//,/\",\"}\"]" + if [ $TEST_CURVES == "True" ]; then + echo -n "," + echo -n "\"curves_ordering\":\"${cipher_arr[9]}\"" + fi + fi + echo -n "}" ctr=$((ctr+1)) done - echo ']}' + echo -n ']' + if [ $TEST_CURVES == "True" ]; then + echo -n ",\"curves_fallback\":\"$fallback_supported\"" + fi + echo '}' } test_serverside_ordering() { @@ -616,6 +730,205 @@ test_serverside_ordering() { fi } +test_curves() { + # "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 + current_curves="" + # return variable: check if server uses server side or client side ordering + # for curves + curves_ordering="server" + + local curves=(${CURVES[*]}) + + 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 $current_cipher" + # force the TLS to send a TLS1.0 client hello at least, as with SSLv2 + # ciphers present it will try to send a SSLv2 compatible client hello + sslcommand+=" -no_ssl2 -no_ssl3" + + # + # 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 with command $sslcommand" + + 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 + break + else + # server accepted connection + local ephem_data=(${current_pfs//,/ }) + local cname="" + if [[ ${ephem_data[0]} =~ ECDH ]]; then + if [ "$current_curves" != "" ]; then + current_curves+="," + fi + cname="$(get_curve_name ${ephem_data[1]})" + verbose "Server selected ${ephem_data[1]}, a.k.a $cname" + current_curves+="$cname" + fi + for id in "${!curves[@]}"; do + if [ "$cname" == ${curves[$id]} ]; then + # we know it's supported, remove it from set of offered ones + unset curves[$id] + break + fi + done + 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=(${current_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]}" + local cname="$(get_curve_name ${ephem_data[1]})" + if [ "$cname" == "$most_wanted" ]; then + curves_ordering="client" + else + curves_ordering="server" + fi + else + # some servers downgrade to non ECDH when curve order is changed + curves_ordering="inconclusive-noecc" + fi + fi + fi +} + +test_curves_fallback() { + # "True" if server supports ciphers that don't use ECC at a lower priority + local fallback_available="False" + # 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="unknown" + + if [ "$ecc_ciphers" == "" ]; then + verbose "No ECC cipher found, can't test curve fallback" + return + fi + + # 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" + # force the TLS to send a TLS1.0 client hello at least, as with SSLv2 + # ciphers present it will try to send a SSLv2 compatible client hello + sslcommand+=" -no_ssl2 -no_ssl3" + + # + # 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 + # + local curves=(${CURVES[*]}) + while [[ ${#curves[@]} -gt 0 ]]; do + OLDIFS="$IFS" + IFS=':' + local test_curves="${curves[*]}" + IFS="$OLDIFS" + verbose "Testing $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 + verbose "Curve fallback failed, server refused connection" + fallback_supported="False" + break + else + # server accepted connection + local ephem_data=(${current_pfs//,/ }) + + if [[ ${ephem_data[0]} =~ ECDH ]]; then + # we got an ecc connection, remove the curve from the list of testable curves + local cname="$(get_curve_name ${ephem_data[1]})" + verbose "Server selected curve $cname" + for id in "${!curves[@]}"; do + if [ "${curves[id]}" == "$cname" ]; then + 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 + done +} + # If no options are given, give usage information and exit (with error code) if [ $# -eq 0 ]; then usage; @@ -671,6 +984,14 @@ do SAVECRT="$2" shift 2 ;; + --curves) + TEST_CURVES="True" + shift 1 + ;; + --no-curves) + TEST_CURVES="False" + shift 1 + ;; --) # End of all options shift break @@ -711,6 +1032,13 @@ if [ ! -x $OPENSSLBIN ]; then fi fi +if [ $TEST_CURVES == "True" ]; then + if [ ! -z "$($OPENSSLBIN s_client -curves 2>&1|head -1|grep 'unknown option')" ]; then + echo "curves testing not available with your version of openssl, disabling it" + TEST_CURVES="False" + fi +fi + if [ $VERBOSE != 0 ] ; then [ -n "$CACERTS" ] && echo "Using trust anchors from $CACERTS" echo "Loading $($OPENSSLBIN ciphers -v $CIPHERSUITE 2>/dev/null|grep Kx|wc -l) ciphersuites from $(echo -n $($OPENSSLBIN version 2>/dev/null))" @@ -730,6 +1058,10 @@ get_cipher_pref $CIPHERSUITE test_serverside_ordering +if [[ $TEST_CURVES == "True" ]]; then + test_curves_fallback +fi + if [ "$OUTPUTFORMAT" == "json" ]; then display_results_in_json else diff --git a/openssl b/openssl index 85b13b3..aeb3feb 100755 Binary files a/openssl and b/openssl differ diff --git a/top1m/parse_results.py b/top1m/parse_results.py index 0da5e42..6c5326a 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,21 @@ for r,d,flist in os.walk(path): if len(results['ciphersuite']) < 1: continue + """ save ECC fallback (new format) """ + if 'curves_fallback' in results: + tempeccfallback = results['curves_fallback'] + + """ save ECC curve stats (old format) """ + 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']: @@ -265,6 +286,16 @@ for r,d,flist in os.walk(path): TLS1_1 = True elif protocol == 'TLSv1.2': TLS1_2 = True + + """ save ECC curves stats """ + if 'curves_ordering' in entry: + tempeccordering = entry['curves_ordering'] + if 'curves' in entry: + for curve in entry['curves']: + tempecccurve[curve] = 1 + if len(entry['curves']) == 1: + tempecccurve[curve + ' Only'] = 1 + json_file.close() """ don't store stats from unusued servers """ @@ -329,6 +360,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 +554,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() {