mirror of
https://github.com/mozilla/cipherscan.git
synced 2024-11-16 20:03:41 +01:00
b5ce264ea3
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.
1024 lines
33 KiB
Bash
Executable File
1024 lines
33 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
# 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
|
|
|
|
# vim: autoindent tabstop=4 shiftwidth=4 expandtab softtabstop=4 filetype=sh
|
|
|
|
DOBENCHMARK=0
|
|
BENCHMARKITER=30
|
|
REALPATH=$(dirname $0)
|
|
# make sure this doesn't error out when readlink -f isn't available (OSX)
|
|
readlink -f $0 &>/dev/null && REALPATH=$(dirname $(readlink -f $0))
|
|
OPENSSLBIN="${REALPATH}/openssl"
|
|
if [ "$(uname -s)" == "Darwin" ]; then
|
|
OPENSSLBIN="${REALPATH}/openssl-darwin64"
|
|
fi
|
|
|
|
# cipherscan requires bash4, which doesn't come by default in OSX
|
|
if [ "$(bash --version |grep 'version 4')" == "" ]; then
|
|
echo "Bash version 4 is required to run cipherscan."
|
|
echo "Please upgrade your version of bash (ex: brew install bash)."
|
|
exit 1
|
|
fi
|
|
|
|
# test that timeout or gtimeout (darwin) are present
|
|
TIMEOUTBIN="$(which timeout)"
|
|
if [ "$TIMEOUTBIN" == "" ]; then
|
|
TIMEOUTBIN="$(which gtimeout)"
|
|
if [ "$TIMEOUTBIN" == "" ]; then
|
|
echo "neither timeout nor gtimeout are present. install coreutils with {apt-get,yum,brew} install coreutils"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# find a list of trusted CAs on the local system, or use the provided list
|
|
if [ -z "$CACERTS" ]; then
|
|
for f in /etc/pki/tls/certs/ca-bundle.crt /etc/ssl/certs/ca-certificates.crt; do
|
|
if [ -e "$f" ]; then
|
|
CACERTS="$f"
|
|
break
|
|
fi
|
|
done
|
|
fi
|
|
if [ ! -e "$CACERTS" ]; then
|
|
CACERTS="$(dirname $0)/ca-bundle.crt"
|
|
fi
|
|
|
|
# RSA ciphers are put at the end to force Google servers to accept ECDSA ciphers
|
|
# (probably a result of a workaround for the bug in Apple implementation of ECDSA)
|
|
CIPHERSUITE="ALL:COMPLEMENTOFALL:+aRSA"
|
|
DEBUG=0
|
|
VERBOSE=0
|
|
DELAY=0
|
|
ALLCIPHERS=0
|
|
OUTPUTFORMAT="terminal"
|
|
TIMEOUT=30
|
|
# place where to put the found intermediate CA certificates and where
|
|
# trust anchors are stored
|
|
CAPATH=""
|
|
SAVECRT=""
|
|
TEST_CURVES="False"
|
|
unset known_certs
|
|
declare -A known_certs
|
|
unset cert_checksums
|
|
declare -A cert_checksums
|
|
|
|
# because running external commands like sleep incurs a fork penalty, we
|
|
# first check if it is necessary
|
|
ratelimit() {
|
|
if [[ $DELAY != "0" ]]; then
|
|
sleep $DELAY
|
|
fi
|
|
}
|
|
|
|
usage() {
|
|
echo -e "usage: $0 [-a|--allciphers] [-b|--benchmark] [--capath directory]
|
|
[--saveca] [--savecrt directory] [-d|--delay seconds] [-D|--debug] [-j|--json]
|
|
[-v|--verbose] [-o|--openssl file] [openssl s_client args] <target:port>
|
|
usage: $0 -h|--help
|
|
|
|
$0 attempts to connect to a target site using all the ciphersuites it knows.
|
|
Julien Vehent [:ulfr] - https://github.com/jvehent/cipherscan
|
|
|
|
Port defaults to 443
|
|
|
|
example: $ $0 www.google.com:443
|
|
|
|
Use one of the options below:
|
|
|
|
-a | --allciphers Test all known ciphers individually at the end.
|
|
-b | --benchmark Activate benchmark mode.
|
|
--capath use CAs from directory (must be in OpenSSL CAdir format)
|
|
--saveca save intermediate certificates in CA directory
|
|
-d | --delay Pause for n seconds between connections
|
|
-D | --debug Output ALL the information.
|
|
-h | --help Shows this help text.
|
|
-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.
|
|
This enables checking smtp/imap/pop3/ftp/xmpp via -starttls
|
|
|
|
EXAMPLES: $0 -starttls xmpp jabber.ccc.de:5222
|
|
"
|
|
}
|
|
|
|
verbose() {
|
|
if [ $VERBOSE != 0 ]; then
|
|
echo "$@" >&2
|
|
fi
|
|
}
|
|
|
|
debug(){
|
|
if [ $DEBUG == 1 ]; then
|
|
echo Debug: "$@" >&2
|
|
set -evx
|
|
fi
|
|
}
|
|
|
|
c_hash() {
|
|
local h=$(${OPENSSLBIN} x509 -hash -noout -in "$1/$2" 2>/dev/null)
|
|
for ((num=0; num<=100; num++)) ; do
|
|
if [[ $1/${h}.${num} -ef $2 ]]; then
|
|
# file already linked, ignore
|
|
break
|
|
fi
|
|
if [[ ! -e $1/${h}.${num} ]]; then
|
|
# file doesn't exist, create a link
|
|
pushd "$1" > /dev/null
|
|
ln -s "$2" "${h}.${num}"
|
|
popd > /dev/null
|
|
break
|
|
fi
|
|
done
|
|
}
|
|
|
|
parse_openssl_output() {
|
|
# clear variables in case matching doesn't hit them
|
|
current_ocspstaple="False"
|
|
current_cipher=""
|
|
current_pfs=""
|
|
current_protocol=""
|
|
current_tickethint="None"
|
|
current_pubkey=0
|
|
current_trusted="False"
|
|
current_sigalg="None"
|
|
|
|
certs_found=0
|
|
current_raw_certificates=()
|
|
|
|
while read line; do
|
|
# check if there isn't OCSP response data (response and responder cert)
|
|
if [[ $line =~ ^====================================== ]]; then
|
|
while read data; do
|
|
# check if there is a OCSP response in output
|
|
if [[ $data =~ OCSP\ Response\ Data ]]; then
|
|
current_ocspstaple="True"
|
|
continue
|
|
fi
|
|
|
|
# skip all data from a OCSP response
|
|
if [[ $data =~ ^====================================== ]]; then
|
|
break
|
|
fi
|
|
done
|
|
continue
|
|
fi
|
|
|
|
# extract selected cipher
|
|
if [[ $line =~ New,\ ]]; then
|
|
local match=($line)
|
|
current_cipher="${match[4]}"
|
|
continue
|
|
fi
|
|
|
|
# extract data about selected temporary key
|
|
if [[ $line =~ Server\ Temp\ Key ]]; then
|
|
local match=($line)
|
|
current_pfs="${match[3]}${match[4]}${match[5]}${match[6]}"
|
|
continue
|
|
fi
|
|
|
|
# extract used protocol
|
|
if [[ $line =~ ^Protocol\ + ]]; then
|
|
local match=($line)
|
|
current_protocol="${match[2]}"
|
|
continue
|
|
fi
|
|
|
|
# extract session ticket hint
|
|
if [[ $line =~ ticket\ lifetime\ hint ]]; then
|
|
local match=($line)
|
|
current_tickethint="${match[5]}"
|
|
continue
|
|
fi
|
|
|
|
# extract size of server public key
|
|
if [[ $line =~ Server\ public\ key\ is\ ]]; then
|
|
local match=($line)
|
|
current_pubkey="${match[4]}"
|
|
continue
|
|
fi
|
|
|
|
# check if connection used trused certificate
|
|
if [[ $line =~ Verify\ return\ code:\ 0 ]]; then
|
|
current_trusted="True"
|
|
continue
|
|
fi
|
|
|
|
# extract certificates
|
|
if [[ $line =~ -----BEGIN\ CERTIFICATE----- ]]; then
|
|
current_raw_certificates[$certs_found]="$line"$'\n'
|
|
while read data; do
|
|
current_raw_certificates[$certs_found]+="$data"$'\n'
|
|
if [[ $data =~ -----END\ CERTIFICATE----- ]]; then
|
|
break
|
|
fi
|
|
done
|
|
certs_found=$((certs_found+1))
|
|
continue
|
|
fi
|
|
done
|
|
|
|
# if we found any certs in output, process the first one and extract
|
|
# the signature algorithm on it (it's the server's certificate)
|
|
if [[ $certs_found -gt 0 ]]; then
|
|
local ossl_out=$(${OPENSSLBIN} x509 -noout -text 2>/dev/null <<<"${current_raw_certificates[0]}")
|
|
while read data; do
|
|
if [[ $data =~ Signature\ Algorithm ]]; then
|
|
local match=($data)
|
|
current_sigalg="${match[2]}"
|
|
fi
|
|
done <<<"$ossl_out"
|
|
fi
|
|
}
|
|
|
|
# Connect to a target host with the selected ciphersuite
|
|
test_cipher_on_target() {
|
|
local sslcommand=$@
|
|
cipher=""
|
|
local cmnd=""
|
|
protocols=""
|
|
pfs=""
|
|
previous_cipher=""
|
|
certificates=""
|
|
for tls_version in "-ssl2" "-ssl3" "-tls1" "-tls1_1" "-tls1_2"
|
|
do
|
|
# sslv2 client hello doesn't support SNI extension
|
|
# in SSLv3 mode OpenSSL just ignores the setting so it's ok
|
|
# -status exception is ignored in SSLv2, go figure
|
|
if [ "$tls_version" == "-ssl2" ]; then
|
|
if [[ "$sslcommand" =~ (.*)(-servername\ [^ ]*)(.*) ]]; then
|
|
cmnd="${BASH_REMATCH[1]} ${BASH_REMATCH[3]}"
|
|
else
|
|
cmnd="$sslcommand"
|
|
fi
|
|
else
|
|
cmnd=$sslcommand
|
|
fi
|
|
ratelimit
|
|
debug echo \"Q\" \| $cmnd $tls_version
|
|
local tmp=$(echo "Q" | $cmnd $tls_version 1>/dev/stdout 2>/dev/null)
|
|
|
|
parse_openssl_output <<<"$tmp"
|
|
verbose "selected cipher is '$current_cipher'"
|
|
verbose "using protocol '$current_protocol'"
|
|
|
|
# collect certificate data
|
|
current_certificates=""
|
|
local certificate_count=$certs_found
|
|
debug "server presented $certificate_count certificates"
|
|
local i
|
|
for ((i=0; i<$certificate_count; i=i+1 )); do
|
|
|
|
# extract i'th certificate
|
|
local cert="${current_raw_certificates[$i]}"
|
|
# put the output to an array instead running awk '{print $1}'
|
|
local cksum=($(cksum <<<"$cert"))
|
|
# compare the values not just checksums so that eventual collision
|
|
# doesn't mess up results
|
|
if [[ ${known_certs[$cksum]} == $cert ]]; then
|
|
if [ -n "${current_certificates}" ]; then
|
|
current_certificates+=","
|
|
fi
|
|
current_certificates+="\"${cert_checksums[$cksum]}\""
|
|
continue
|
|
fi
|
|
|
|
# compute sha256 fingerprint of the certificate
|
|
local sha256sum=($(${OPENSSLBIN} x509 -outform DER\
|
|
<<<"$cert" 2>/dev/null |\
|
|
${OPENSSLBIN} dgst -sha256 -r 2>/dev/null))
|
|
|
|
# check if it is a CA certificate
|
|
local isCA="False"
|
|
if ${OPENSSLBIN} x509 -noout -text <<<"$cert" 2>/dev/null |\
|
|
grep 'CA:TRUE' >/dev/null; then
|
|
isCA="True"
|
|
fi
|
|
|
|
# build trust source for certificate verification
|
|
local trust_source=()
|
|
if [[ -n $CAPATH ]]; then
|
|
trust_source=("-CApath" "$CAPATH")
|
|
elif [[ -e $CACERTS ]]; then
|
|
trust_source=("-CAfile" "$CACERTS")
|
|
fi
|
|
|
|
# check if the certificate is actually trusted (server may present
|
|
# unrelated certificates that are not trusted (including self
|
|
# signed ones)
|
|
local saved="False"
|
|
if ${OPENSSLBIN} verify "${trust_source[@]}" \
|
|
-untrusted <(printf "%s" "${current_raw_certificates[@]}") <(echo "$cert") 2>/dev/null | \
|
|
grep ': OK$' >/dev/null; then
|
|
|
|
# if the certificate is an intermediate CA it may be useful
|
|
# for connecting to servers that are misconfigured so save it
|
|
if [[ -n $CAPATH ]] && [[ $SAVECA == "True" ]] && [[ $isCA == "True" ]]; then
|
|
if [[ ! -e "$CAPATH/${sha256sum}.pem" ]]; then
|
|
echo "$cert" > "$CAPATH/${sha256sum}.pem"
|
|
c_hash "$CAPATH" "${sha256sum}.pem"
|
|
fi
|
|
saved="True"
|
|
fi
|
|
fi
|
|
if [[ -n $SAVECRT ]] && [[ $saved == "False" ]]; then
|
|
if [[ ! -e $SAVECRT/${sha256sum}.pem ]]; then
|
|
echo "$cert" > "$SAVECRT/${sha256sum}.pem"
|
|
fi
|
|
fi
|
|
# save the sha sum for reporting
|
|
if [ -n "${current_certificates}" ]; then
|
|
current_certificates+=","
|
|
fi
|
|
current_certificates+="\"${sha256sum}\""
|
|
known_certs[$cksum]="$cert"
|
|
cert_checksums[$cksum]="$sha256sum"
|
|
done
|
|
debug "current_certificates: $current_certificates"
|
|
|
|
# parsing finished, report result
|
|
if [[ -z "$current_protocol" || "$current_cipher" == '(NONE)' ]]; then
|
|
# connection failed, try again with next TLS version
|
|
continue
|
|
else
|
|
verbose "connection successful; protocol: $current_protocol, cipher: $current_cipher, previous cipher: $previous_cipher"
|
|
fi
|
|
# handling of TLSv1.2 only cipher suites
|
|
if [ ! -z "$previous_cipher" ] && [ "$previous_cipher" != "$current_cipher" ] && [ "$current_cipher" != "0000" ]; then
|
|
unset protocols
|
|
fi
|
|
previous_cipher=$current_cipher
|
|
|
|
# connection succeeded, add TLS version to positive results
|
|
if [ -z "$protocols" ]; then
|
|
protocols=$current_protocol
|
|
else
|
|
protocols="$protocols,$current_protocol"
|
|
fi
|
|
cipher=$current_cipher
|
|
pfs=$current_pfs
|
|
pubkey=$current_pubkey
|
|
sigalg=$current_sigalg
|
|
trusted=$current_trusted
|
|
tickethint=$current_tickethint
|
|
ocspstaple=$current_ocspstaple
|
|
certificates="$current_certificates"
|
|
# grab the cipher and PFS key size
|
|
done
|
|
# if cipher is empty, that means none of the TLS version worked with
|
|
# the current cipher
|
|
if [ -z "$cipher" ]; then
|
|
verbose "handshake failed, no ciphersuite was returned"
|
|
result='ConnectionFailure'
|
|
return 2
|
|
|
|
# if cipher contains NONE, the cipher wasn't accepted
|
|
elif [ "$cipher" == '(NONE) ' ]; then
|
|
result="$cipher $protocols $pubkey $sigalg $trusted $tickethint $ocspstaple $pfs"
|
|
verbose "handshake failed, server returned ciphersuite '$result'"
|
|
return 1
|
|
|
|
# the connection succeeded
|
|
else
|
|
result="$cipher $protocols $pubkey $sigalg $trusted $tickethint $ocspstaple $pfs"
|
|
verbose "handshake succeeded, server returned ciphersuite '$result'"
|
|
return 0
|
|
fi
|
|
}
|
|
|
|
# Calculate the average handshake time for a specific ciphersuite
|
|
bench_cipher() {
|
|
local ciphersuite="$1"
|
|
local sslcommand="$TIMEOUTBIN $TIMEOUT $OPENSSLBIN s_client $SCLIENTARGS -connect $TARGET -cipher $ciphersuite"
|
|
local t="$(date +%s%N)"
|
|
verbose "Benchmarking handshake on '$TARGET' with ciphersuite '$ciphersuite'"
|
|
for i in $(seq 1 $BENCHMARKITER); do
|
|
debug Connection $i
|
|
(echo "Q" | $sslcommand 2>/dev/null 1>/dev/null)
|
|
if [ $? -gt 0 ]; then
|
|
break
|
|
fi
|
|
done
|
|
# Time interval in nanoseconds
|
|
local t="$(($(date +%s%N) - t))"
|
|
verbose "Benchmarking done in $t nanoseconds"
|
|
# Microseconds
|
|
cipherbenchms="$((t/1000/$BENCHMARKITER))"
|
|
}
|
|
|
|
# Connect to the target and retrieve the chosen cipher
|
|
# recursively until the connection fails
|
|
get_cipher_pref() {
|
|
[ "$OUTPUTFORMAT" == "terminal" ] && [ $DEBUG -lt 1 ] && echo -n '.'
|
|
local ciphersuite="$1"
|
|
|
|
local 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 $ciphersuite"
|
|
|
|
verbose "Connecting to '$TARGET' with ciphersuite '$ciphersuite'"
|
|
test_cipher_on_target "$sslcommand"
|
|
local success=$?
|
|
# If the connection succeeded with the current cipher, benchmark and store
|
|
if [ $success -eq 0 ]; then
|
|
cipherspref=("${cipherspref[@]}" "$result")
|
|
ciphercertificates=("${ciphercertificates[@]}" "$certificates")
|
|
pciph=($result)
|
|
get_cipher_pref "!$pciph:$ciphersuite"
|
|
return 0
|
|
fi
|
|
}
|
|
|
|
display_results_in_terminal() {
|
|
# Display the results
|
|
ctr=1
|
|
local pubkey
|
|
local sigalg
|
|
local trusted
|
|
local tickethint
|
|
local ocspstaple
|
|
local different=False
|
|
echo "Target: $TARGET"; echo
|
|
for cipher in "${cipherspref[@]}"; do
|
|
# get first in array
|
|
pciph=($cipher)
|
|
if [ $DOBENCHMARK -eq 1 ]; then
|
|
bench_cipher "$pciph"
|
|
r="$ctr $cipher $cipherbenchms"
|
|
else
|
|
r="$ctr $cipher"
|
|
fi
|
|
local cipher_data=($cipher)
|
|
if [ $ctr -eq 1 ]; then
|
|
pubkey="${cipher_data[2]}"
|
|
sigalg="${cipher_data[3]}"
|
|
trusted="${cipher_data[4]}"
|
|
tickethint="${cipher_data[5]}"
|
|
ocspstaple="${cipher_data[6]}"
|
|
else
|
|
if [ "$pubkey" != "${cipher_data[2]}" ]; then
|
|
different=True
|
|
fi
|
|
if [ "$sigalg" != "${cipher_data[3]}" ]; then
|
|
different=True
|
|
fi
|
|
if [ "$trusted" != "${cipher_data[4]}" ]; then
|
|
different=True
|
|
fi
|
|
if [ "$tickethint" != "${cipher_data[5]}" ]; then
|
|
different=True
|
|
fi
|
|
fi
|
|
results=("${results[@]}" "$r")
|
|
ctr=$((ctr+1))
|
|
done
|
|
|
|
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
|
|
fi
|
|
ctr=0
|
|
for result in "${results[@]}"; do
|
|
if [ $ctr -eq 0 ]; then
|
|
echo $header
|
|
ctr=$((ctr+1))
|
|
fi
|
|
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"
|
|
fi
|
|
done|column -t
|
|
echo
|
|
if [ $different != "True" ]; then
|
|
if [ "$trusted" == "True" ]; then
|
|
echo "Certificate: trusted, $pubkey bit, $sigalg signature"
|
|
else
|
|
echo "Certificate: UNTRUSTED, $pubkey bit, $sigalg signature"
|
|
fi
|
|
echo "TLS ticket lifetime hint: $tickethint"
|
|
fi
|
|
if [[ $ocspstaple == "True" ]]; then
|
|
echo "OCSP stapling: supported"
|
|
else
|
|
echo "OCSP stapling: not supported"
|
|
fi
|
|
if [[ $serverside == "True" ]]; then
|
|
echo "Server side cipher ordering"
|
|
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() {
|
|
# Display the results in json
|
|
ctr=0
|
|
echo -n "{\"target\":\"$TARGET\",\"utctimestamp\":\"$(date -u '+%FT%T.0Z')\",\"serverside\":\"${serverside}\",\"ciphersuite\": ["
|
|
for cipher in "${cipherspref[@]}"; do
|
|
local cipher_arr=($cipher)
|
|
[ $ctr -gt 0 ] && echo -n ','
|
|
echo -n "{\"cipher\":\"${cipher_arr[0]}\","
|
|
echo -n "\"protocols\":[\"${cipher_arr[1]//,/\",\"}\"],"
|
|
echo -n "\"pubkey\":[\"${cipher_arr[2]//,/\",\"}\"],"
|
|
echo -n "\"sigalg\":[\"${cipher_arr[3]//,/\",\"}\"],"
|
|
echo -n "\"trusted\":\"${cipher_arr[4]//,/\",\"}\","
|
|
if [[ -n $CAPATH ]]; then
|
|
echo -n "\"certificates\":[${ciphercertificates[$ctr]}],"
|
|
fi
|
|
echo -n "\"ticket_hint\":\"${cipher_arr[5]}\","
|
|
echo -n "\"ocsp_stapling\":\"${cipher_arr[6]}\","
|
|
pfs="${cipher_arr[7]}"
|
|
[ "$pfs" == "" ] && pfs="None"
|
|
echo -n "\"pfs\":\"$pfs\"}"
|
|
ctr=$((ctr+1))
|
|
done
|
|
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() {
|
|
local ciphersuite=""
|
|
local prefered=""
|
|
# server supports only one cipher or no ciphers, so it effectively uses server side ordering...
|
|
if [[ ${#cipherspref[@]} -lt 2 ]]; then
|
|
serverside="True"
|
|
return 0
|
|
# server supports just two ciphers, so rotate them, that should be enough
|
|
elif [[ ${#cipherspref[@]} -eq 2 ]]; then
|
|
|
|
local cipher=(${cipherspref[1]})
|
|
prefered="$cipher"
|
|
ciphersuite=$cipher
|
|
|
|
cipher=(${cipherspref[0]})
|
|
ciphersuite+=":$cipher"
|
|
|
|
# server supports 3 or more ciphers, rotate all three. This is necessary because google does
|
|
# select first client provided cipher, if it is either CDHE-RSA-AES128-GCM-SHA256 or
|
|
# ECDHE-RSA-CHACHA20-POLY1305
|
|
else
|
|
local cipher=(${cipherspref[2]})
|
|
prefered="$cipher"
|
|
ciphersuite="$cipher"
|
|
|
|
cipher=(${cipherspref[1]})
|
|
ciphersuite+=":$cipher"
|
|
|
|
cipher=(${cipherspref[0]})
|
|
ciphersuite+=":$cipher"
|
|
fi
|
|
|
|
local 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 $ciphersuite"
|
|
|
|
test_cipher_on_target "$sslcommand"
|
|
if [ $? -ne 0 ]; then
|
|
serverside="True"
|
|
else
|
|
local selected=($result)
|
|
if [[ $selected == $prefered ]]; then
|
|
serverside="False"
|
|
else
|
|
serverside="True"
|
|
fi
|
|
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;
|
|
exit 1
|
|
fi
|
|
|
|
# UNKNOWNOPTIONS=""
|
|
while :
|
|
do
|
|
case $1 in
|
|
-h | --help | -\?)
|
|
usage
|
|
exit 0 # This is not an error, User asked help. Don't do "exit 1"
|
|
;;
|
|
-o | --openssl)
|
|
OPENSSLBIN=$2 # You might want to check if you really got FILE
|
|
shift 2
|
|
;;
|
|
-a | --allciphers)
|
|
ALLCIPHERS=1
|
|
shift
|
|
;;
|
|
-v | --verbose)
|
|
# Each instance of -v adds 1 to verbosity
|
|
VERBOSE=$((VERBOSE+1))
|
|
shift
|
|
;;
|
|
-j | -json | --json | --JSON)
|
|
OUTPUTFORMAT="json"
|
|
shift
|
|
;;
|
|
-b | --benchmark)
|
|
DOBENCHMARK=1
|
|
shift
|
|
;;
|
|
-D | --debug)
|
|
DEBUG=1
|
|
shift
|
|
;;
|
|
-d | --delay)
|
|
DELAY=$2
|
|
shift 2
|
|
;;
|
|
--capath)
|
|
CAPATH="$2"
|
|
shift 2
|
|
;;
|
|
--saveca)
|
|
SAVECA="True"
|
|
shift 1
|
|
;;
|
|
--savecrt)
|
|
SAVECRT="$2"
|
|
shift 2
|
|
;;
|
|
--curves)
|
|
TEST_CURVES="True"
|
|
shift 1
|
|
;;
|
|
--) # End of all options
|
|
shift
|
|
break
|
|
;;
|
|
# -*)
|
|
# UNKNOWNOPTIONS=$((UNKNOWNOPTIONS+$1))
|
|
# # echo "WARN: Unknown option (ignored): $1" >&2
|
|
# shift
|
|
# ;;
|
|
*) # no more options we understand.
|
|
break
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# echo parameters left: $@
|
|
|
|
TEMPTARGET=$(sed -e 's/^.* //'<<<"${@}")
|
|
HOST=$(sed -e 's/:.*//'<<<"${TEMPTARGET}")
|
|
PORT=$(sed -e 's/.*://'<<<"${TEMPTARGET}")
|
|
|
|
# Default to https if no port given
|
|
if [ "$HOST" = "$PORT" ]; then
|
|
PORT=443
|
|
fi
|
|
|
|
debug "host: $HOST"
|
|
debug "Port: $PORT"
|
|
|
|
TARGET=$HOST:$PORT
|
|
debug "target: $TARGET"
|
|
|
|
# test our openssl is usable
|
|
if [ ! -x $OPENSSLBIN ]; then
|
|
OPENSSLBIN=$(which openssl)
|
|
if [ "$OUTPUTFORMAT" == "terminal" ]; then
|
|
echo "custom openssl not executable, falling back to system one from $OPENSSLBIN"
|
|
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))"
|
|
$OPENSSLBIN ciphers ALL 2>/dev/null
|
|
fi
|
|
|
|
SCLIENTARGS=$(sed -e s,${TEMPTARGET},,<<<"${@}")
|
|
debug "sclientargs: $SCLIENTARGS"
|
|
|
|
|
|
cipherspref=();
|
|
ciphercertificates=()
|
|
results=()
|
|
|
|
# Call to the recursive loop that retrieves the cipher preferences
|
|
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
|
|
echo
|
|
display_results_in_terminal
|
|
fi
|
|
|
|
# If asked, test every single cipher individually
|
|
if [ $ALLCIPHERS -gt 0 ]; then
|
|
echo; echo "All accepted ciphersuites"
|
|
for c in $($OPENSSLBIN ciphers -v ALL:COMPLEMENTOFALL 2>/dev/null |awk '{print $1}'|sort|uniq); do
|
|
r="fail"
|
|
osslcommand="$TIMEOUTBIN $TIMEOUT $OPENSSLBIN s_client $SCLIENTARGS -connect $TARGET -cipher $c"
|
|
test_cipher_on_target "$osslcommand"
|
|
if [ $? -eq 0 ]; then
|
|
r="pass"
|
|
fi
|
|
echo "$c $r"|awk '{printf "%-35s %s\n",$1,$2}'
|
|
done
|
|
fi
|