From 434b383f016df5457a78c34428c2925dbe72f955 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Sat, 1 Nov 2014 20:26:31 +0100 Subject: [PATCH] add test for TLSv1.2 PFS key exchange since the signature and hash algorithm in TLSv1.2 is selectable by server and negotiated using TLS extensions, we can check what sig algs is the server willing to perform and whatever it does honour client selection it also tests what happens if the client doesn't offer any sigalgs that are necessary to use the ciphers selected by server --- cipherscan | 329 ++++++++++++++++++++++++++++++++++++++++- top1m/parse_results.py | 38 +++++ top1m/testtop1m.sh | 4 +- 3 files changed, 367 insertions(+), 4 deletions(-) diff --git a/cipherscan b/cipherscan index 81406fa..0c032b8 100755 --- a/cipherscan +++ b/cipherscan @@ -4,7 +4,7 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. # Authors: Julien Vehent [:ulfr] - 201{3,4} -# Hubert Kario - 2014 +# Hubert Kario - 2014, 2015 # vim: autoindent tabstop=4 shiftwidth=4 expandtab softtabstop=4 filetype=sh @@ -207,12 +207,18 @@ TEST_TOLERANCE="True" SNI="True" # openssl formated list of curves that will cause server to select ECC suite ecc_ciphers="" +TEST_KEX_SIGALG="False" unset known_certs declare -A known_certs unset cert_checksums declare -A cert_checksums # array with results of tolerance scans (TLS version, extensions, etc.) declare -A tls_tolerance +# array with info on type of fallback on unknown sigalgs (or required ones) +declare -A sigalgs_fallback +# array with preferred sigalgs for aRSA and aECDSA ciphers +declare -a sigalgs_preferred_rsa +declare -a sigalgs_preferred_ecdsa # because running external commands like sleep incurs a fork penalty, we # first check if it is necessary @@ -248,6 +254,8 @@ Use one of the options below: -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) +--sigalg test signature algorithms used in TLSv1.2 ephemeral ciphers + (req. OpenSSL 1.0.2) --[no-]tolerance test TLS tolerance --no-sni don't use Server Name Indication --no-colors don't use terminal colors @@ -367,6 +375,7 @@ parse_openssl_output() { # clear variables in case matching doesn't hit them current_ocspstaple="False" current_cipher="" + current_kex_sigalg="" current_pfs="" current_protocol="" current_tickethint="None" @@ -402,6 +411,13 @@ parse_openssl_output() { continue fi + # extract the signing algorithm used in TLSv1.2 ephemeral kex + if [[ $line =~ Peer\ signing\ digest ]]; then + local match=($line) + current_kex_sigalg="${match[3]}" + continue + fi + # extract data about selected temporary key if [[ $line =~ Server\ Temp\ Key ]]; then local match=($line) @@ -679,6 +695,21 @@ get_cipher_pref() { fi } +display_sigalgs_in_terminal() { + (echo "prio sigalg" + for sigalg in "$@"; do + if [[ $sigalg == "MD5" ]]; then + color="${c_red}" + elif [[ $sigalg == "SHA1" ]]; then + color="${c_yellow}" + else + color="${c_green}" + fi + echo -e "$cnt ${color}$sigalg${c_reset}" + cnt=$((cnt+1)) + done )| column -t +} + display_results_in_terminal() { # Display the results ctr=1 @@ -693,13 +724,15 @@ display_results_in_terminal() { if [[ $USECOLORS == "True" && -x /usr/bin/tput ]] && tput setaf 1 >&/dev/null; then c_blue="\033[0;34m" c_green="\033[0;32m" + c_yellow="\033[0;33m" c_red="\033[0;31m" c_reset="\033[0m" else - c_reset= c_blue= c_green= + c_yellow= c_red= + c_reset= fi echo "Target: $TARGET"; echo @@ -824,7 +857,49 @@ display_results_in_terminal() { echo -e "Curves ordering: $curvesordering - fallback: $fallback_supported" fi + if [[ $TEST_KEX_SIGALG == "True" ]]; then + echo + echo "TLSv1.2 ephemeral sigalgs:" + for auth in "ECDSA" "RSA"; do + # not colored as neither of that results alone is good or bad + if [[ -z ${sigalgs_fallback[$auth]} ]]; then + echo "no PFS $auth ciphers detected" + elif [[ ${sigalgs_fallback[$auth]} == "False" ]]; then + echo "no PFS $auth fallback" + elif [[ ${sigalgs_fallback[$auth]} == "intolerant" ]]; then + echo "$auth test: intolerant of sigalg removal" + elif [[ ${sigalgs_fallback[$auth]} =~ "pfs-" ]]; then + echo "PFS $auth fallbacks to ${sigalgs_fallback[$auth]}" + else + echo "server forces ${sigalgs_fallback[$auth]} with $auth" + fi + done + + if [[ ${#sigalgs_preferred_ecdsa[@]} -gt 0 ]]; then + echo + if [[ ${sigalgs_preferred_ecdsa[0]} == "Fail" ]]; then + echo -e "${c_red}ECDSA test failed${c_reset}" + else + local cnt=1 + echo "Supported PFS ECDSA signature algorithms" + display_sigalgs_in_terminal "${sigalgs_preferred_ecdsa[@]}" + fi + fi + + if [[ ${#sigalgs_preferred_rsa[@]} -gt 0 ]]; then + echo + if [[ ${sigalgs_preferred_rsa[0]} == "Fail" ]]; then + echo -e "${c_red}RSA test failed${c_reset}" + else + local cnt=1 + echo "Supported PFS RSA signature algorithms" + display_sigalgs_in_terminal "${sigalgs_preferred_rsa[@]}" + fi + fi + fi + if [[ $TEST_TOLERANCE == "True" ]]; then + echo if [[ $tls_tolerance['big-TLSv1.2'] =~ TLSv1.2 ]]; then echo -e "TLS Tolerance: ${c_green}yes${c_reset}" else @@ -877,6 +952,40 @@ display_results_in_json() { if [[ $TEST_CURVES == "True" ]]; then echo -n ",\"curves_fallback\":\"$fallback_supported\"" fi + + if [[ $TEST_KEX_SIGALG == "True" ]]; then + echo -n ',"sigalgs":{' + echo -n "\"ECDSA-fallback\":\"${sigalgs_fallback[ECDSA]}\"," + echo -n "\"RSA-fallback\":\"${sigalgs_fallback[RSA]}\"" + if [[ ${#sigalgs_preferred_ecdsa[@]} -gt 0 ]]; then + echo -n "," + echo -n '"ECDSA":[' + local cnt=0 + for sigalg in "${sigalgs_preferred_ecdsa[@]}"; do + if [[ $cnt -gt 0 ]]; then + echo -n ',' + fi + echo -n "\"$sigalg\"" + cnt=$((cnt+1)) + done + echo -n ']' + fi + if [[ ${#sigalgs_preferred_rsa[@]} -gt 0 ]]; then + echo -n "," + echo -n '"RSA":[' + local cnt=0 + for sigalg in "${sigalgs_preferred_rsa[@]}"; do + if [[ $cnt -gt 0 ]]; then + echo -n ',' + fi + echo -n "\"$sigalg\"" + cnt=$((cnt+1)) + done + echo -n ']' + fi + echo -n '}' + fi + echo -n ',"configs":{' ctr=0 for test_name in "${!tls_tolerance[@]}"; do @@ -1345,6 +1454,214 @@ test_tls_tolerance() { fi } +test_kex_sigalgs() { + local ecdsa_sigalgs=("ECDSA+SHA512" "ECDSA+SHA384" "ECDSA+SHA256" + "ECDSA+SHA224" "ECDSA+SHA1" "ECDSA+MD5") + local rsa_sigalgs=("RSA+SHA512" "RSA+SHA384" "RSA+SHA256" + "RSA+SHA224" "RSA+SHA1" "RSA+MD5") + + local supported_rsa_ciphers="" + local supported_ecdsa_ciphers="" + local supported_ciphers=() + + # check if TLS1.2 is supported by server, as tests needs it + # collect ciphers + local tls12="False" + for cipher in "${cipherspref[@]}"; do + local ciph_data=($cipher) + if [[ ${ciph_data[1]} =~ TLSv1.2 ]]; then + tls12="True" + fi + supported_ciphers+=(${ciph_data[0]}) + done + + if [[ $tls12 == "False" ]]; then + return + fi + + # create cipher list for ecdsa and rsa tests that include non ephemeral + # ciphers for fallback + for cipher in "${supported_ciphers[@]}"; do + if [[ $cipher =~ DHE-ECDSA ]]; then + if [[ $supported_ecdsa_ciphers ]]; then + supported_ecdsa_ciphers+=":" + fi + supported_ecdsa_ciphers+="$cipher" + elif [[ ${supported_ecdsa_ciphers} ]]; then + supported_ecdsa_ciphers+=":$cipher" + fi + + if [[ $cipher =~ DHE-RSA ]]; then + if [[ $supported_rsa_ciphers ]]; then + supported_rsa_ciphers+=":" + fi + supported_rsa_ciphers+="$cipher" + elif [[ ${supported_rsa_ciphers} ]]; then + supported_rsa_ciphers+=":$cipher" + fi + done + + # + # Test default sigalgs for aECDSA ciphers + # + if [[ $supported_ecdsa_ciphers ]]; then + local sslcommand="$TIMEOUTBIN $TIMEOUT $OPENSSLBIN s_client" + if [ -n "$CAPATH" ]; then + sslcommand+=" -CApath $CAPATH -showcerts" + elif [ -e "$CACERTS" ]; then + sslcommand+=" -CAfile $CACERTS" + fi + sslcommand+=" $SCLIENTARGS -connect $TARGET -cipher $supported_ecdsa_ciphers" + # since some ciphers supported by server may be SSLv2 only, we need to + # force use of TLSv1.2, otherwise openssl will send a SSLv2 compatible + # client hello + sslcommand+=" -no_ssl2 -no_ssl3" + local test_ecdsa_sigalgs=("${ecdsa_sigalgs[@]}") + local test_rsa_sigalgs=("${rsa_sigalgs[@]}") + + while true; do + join_array_by_char ":" "${test_ecdsa_sigalgs[@]}" \ + "${test_rsa_sigalgs[@]}" + local test_sigalgs="$joined_array" + + ratelimit + verbose "Testing default ECDSA sig algs with $sslcommand -sigalgs $test_sigalgs" + local tmp=$(echo Q | $sslcommand -sigalgs $test_sigalgs 2>/dev/null) + parse_openssl_output <<<"$tmp" + verbose "server selected $current_cipher, $current_protocol, $current_kex_sigalg" + if [[ -z $current_protocol || $current_cipher == "(NONE)" \ + || $current_cipher == "0000" ]]; then + if [[ ${#test_ecdsa_sigalgs[@]} -eq 0 ]]; then + sigalgs_fallback["ECDSA"]="False" + else + sigalgs_fallback["ECDSA"]="intolerant" + fi + break + fi + + if [[ $current_cipher =~ DHE-ECDSA ]]; then + if [[ -z $current_kex_sigalg ]]; then + # if we didn't get a sigalg that means the test failed + sigalgs_preferred_ecdsa=("Fail") + break + fi + + if [[ "${test_ecdsa_sigalgs[*]}" =~ "ECDSA+$current_kex_sigalg" ]]; then + # save sigalg for reporting + sigalgs_preferred_ecdsa+=("$current_kex_sigalg") + + # remove it from offered + local id + for id in "${!test_ecdsa_sigalgs[@]}"; do + if [[ ${test_ecdsa_sigalgs[$id]} =~ $current_kex_sigalg ]]; then + unset test_ecdsa_sigalgs[$id] + break + fi + done + # continue testing + else + # server selected sigalg we didn't offer + sigalgs_fallback["ECDSA"]="$current_kex_sigalg" + break + fi + elif [[ $current_cipher =~ DHE-RSA ]]; then + # we got a fallback to a RSA based cipher + if [[ -z $current_kex_sigalg ]]; then + sigalgs_fallback["ECDSA"]="pfs-rsa" + else + sigalgs_fallback["ECDSA"]="pfs-rsa-${current_kex_sigalg}" + fi + break + else + # we got a fallback to a non PFS cipher, that's good too + sigalgs_fallback["ECDSA"]="soft-nopfs" + break + fi + [ "$OUTPUTFORMAT" == "terminal" ] && [ $DEBUG -lt 1 ] && echo -n '.' + done + fi + + # + # Test default sigalgs for aRSA ciphers + # + if [[ ${supported_rsa_ciphers} ]]; then + local sslcommand="$TIMEOUTBIN $TIMEOUT $OPENSSLBIN s_client" + if [ -n "$CAPATH" ]; then + sslcommand+=" -CApath $CAPATH -showcerts" + elif [ -e "$CACERTS" ]; then + sslcommand+=" -CAfile $CACERTS" + fi + sslcommand+=" $SCLIENTARGS -connect $TARGET -cipher $supported_rsa_ciphers" + # since some ciphers supported by server may be SSLv2 only, we need to + # force use of TLSv1.2, otherwise openssl will send a SSLv2 compatible + # client hello + sslcommand+=" -no_ssl2 -no_ssl3" + local test_ecdsa_sigalgs=("${ecdsa_sigalgs[@]}") + local test_rsa_sigalgs=("${rsa_sigalgs[@]}") + + while true; do + join_array_by_char ":" "${test_rsa_sigalgs[@]}" \ + "${test_ecdsa_sigalgs[@]}" + local test_sigalgs="$joined_array" + + ratelimit + verbose "Testing default RSA sig algs with $sslcommand -sigalgs $test_sigalgs" + local tmp=$(echo Q | $sslcommand -sigalgs $test_sigalgs 2>/dev/null) + parse_openssl_output <<<"$tmp" + verbose "server selected $current_cipher, $current_protocol, $current_kex_sigalg" + if [[ -z $current_protocol || $current_cipher == "(NONE)" \ + || $current_cipher == "0000" ]]; then + if [[ ${#test_rsa_sigalgs[@]} -eq 0 ]]; then + sigalgs_fallback["RSA"]="False" + else + sigalgs_fallback["RSA"]="intolerant" + fi + break + fi + + if [[ $current_cipher =~ DHE-RSA ]]; then + if [[ -z $current_kex_sigalg ]]; then + # if we didn't get a sigalg that means the test failed + sigalgs_preferred_rsa=("Fail") + break + fi + + if [[ "${test_rsa_sigalgs[*]}" =~ "RSA+$current_kex_sigalg" ]]; then + # save sigalg for reporting + sigalgs_preferred_rsa+=("$current_kex_sigalg") + + # remove it from offered + local id + for id in "${!test_rsa_sigalgs[@]}"; do + if [[ ${test_rsa_sigalgs[$id]} =~ $current_kex_sigalg ]]; then + unset test_rsa_sigalgs[$id] + break + fi + done + # continue testing + else + # server selected sigalg we didn't offer + sigalgs_fallback["RSA"]="$current_kex_sigalg" + break + fi + elif [[ $current_cipher =~ DHE-ECDSA ]]; then + # we got a fallback to an ECDSA based cipher + if [[ -z $current_kex_sigalg ]]; then + sigalgs_fallback["RSA"]="pfs-ecdsa" + else + sigalgs_fallback["RSA"]="pfs-ecdsa-${current_kex_sigalg}" + fi + break + else + # we got a fallback to a non PFS cipher, that's good too + sigalgs_fallback["RSA"]="soft-nopfs" + break + fi + [ "$OUTPUTFORMAT" == "terminal" ] && [ $DEBUG -lt 1 ] && echo -n '.' + done + fi +} + # If no options are given, give usage information and exit (with error code) if (( $# == 0 )); then usage @@ -1414,6 +1731,10 @@ do TEST_CURVES="False" shift 1 ;; + --sigalg) + TEST_KEX_SIGALG="True" + shift 1 + ;; --tolerance) TEST_TOLERANCE="True" shift 1 @@ -1567,6 +1888,10 @@ fi test_serverside_ordering +if [[ $TEST_KEX_SIGALG == "True" ]]; then + test_kex_sigalgs +fi + if [[ $TEST_CURVES == "True" ]]; then test_curves_fallback fi diff --git a/top1m/parse_results.py b/top1m/parse_results.py index e4ba944..99b5643 100644 --- a/top1m/parse_results.py +++ b/top1m/parse_results.py @@ -135,6 +135,8 @@ fallback_ids['v2-big-TLSv1.2'] = i i+=1 # 3rd padding space fallback_ids[' '] = i +pfssigalgfallback = defaultdict(int) +pfssigalgs = defaultdict(int) dsarsastack = 0 total = 0 for r,d,flist in os.walk(path): @@ -155,6 +157,8 @@ for r,d,flist in os.walk(path): tempfallbacks = {} """ supported ciphers by the server under scan """ tempcipherstats = {} + temppfssigalgfallback = {} + temppfssigalgs = {} ciphertypes = 0 AESGCM = False AESCBC = False @@ -249,6 +253,23 @@ for r,d,flist in os.walk(path): if len(results['curve']) == 1: tempecccurve[curve + ' Only'] = 1 + """ collect TLSv1.2 PFS ciphersuite sigalgs """ + if 'sigalgs' in results: + if results['sigalgs']['ECDSA-fallback']: + temppfssigalgfallback['ECDSA ' + results['sigalgs']['ECDSA-fallback']] = 1 + if results['sigalgs']['RSA-fallback']: + temppfssigalgfallback['RSA ' + results['sigalgs']['RSA-fallback']] = 1 + if 'RSA' in results['sigalgs'] and results['sigalgs']['RSA'][0] != 'Fail': + for pfssigalg in results['sigalgs']['RSA']: + temppfssigalgs['RSA-' + pfssigalg]=1 + if len(results['sigalgs']['RSA']) == 1: + temppfssigalgs['RSA-' + results['sigalgs']['RSA'][0] + ' Only'] = 1 + if 'ECDSA' in results['sigalgs'] and results['sigalgs']['ECDSA'][0] != 'Fail': + for pfssigalg in results['sigalgs']['ECDSA']: + temppfssigalgs['ECDSA-' + pfssigalg]=1 + if len(results['sigalgs']['ECDSA']) == 1: + temppfssigalgs['ECDSA-' + results['sigalgs']['ECDSA'][0] + ' Only'] = 1 + if 'configs' in results: tolerance = [' '] * len(fallback_ids) for entry in results['configs']: @@ -531,6 +552,11 @@ for r,d,flist in os.walk(path): else: ocspstaple['Unsupported'] += 1 + for s in temppfssigalgfallback: + pfssigalgfallback[s] += 1 + for s in temppfssigalgs: + pfssigalgs[s] += 1 + """ store cipher stats """ if AESGCM: cipherstats['AES-GCM'] += 1 @@ -735,6 +761,18 @@ 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("\nTLSv1.2 PFS supported sigalgs Count Percent ") +print("------------------------------+---------+--------") +for stat in sorted(pfssigalgs): + percent = round(pfssigalgs[stat] / total * 100, 4) + sys.stdout.write(stat.ljust(30) + " " + str(pfssigalgs[stat]).ljust(10) + str(percent).ljust(9) + "\n") + +print("\nTLSv1.2 PFS sigalg fallback Count Percent ") +print("------------------------------+---------+--------") +for stat in sorted(pfssigalgfallback): + percent = round(pfssigalgfallback[stat] / total * 100, 4) + sys.stdout.write(stat.ljust(30) + " " + str(pfssigalgfallback[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 faf8f26..14f4765 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 --curves --savecrt certs --delay 2 -json -servername $1 $2:443 > results/$1@$2 + ../cipherscan --capath ca_files --saveca --curves --savecrt certs --delay 2 --sigalg -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 --curves --savecrt certs --delay 2 -json $1:443 > results/$1 + ../cipherscan --capath ca_files --saveca --curves --savecrt certs --delay 2 --sigalg -json $1:443 > results/$1 } function scan_hostname() {