summaryrefslogtreecommitdiffstats
path: root/deluge/tests/data/testssl.sh
diff options
context:
space:
mode:
Diffstat (limited to 'deluge/tests/data/testssl.sh')
-rwxr-xr-xdeluge/tests/data/testssl.sh20256
1 files changed, 20256 insertions, 0 deletions
diff --git a/deluge/tests/data/testssl.sh b/deluge/tests/data/testssl.sh
new file mode 100755
index 0000000..c04d055
--- /dev/null
+++ b/deluge/tests/data/testssl.sh
@@ -0,0 +1,20256 @@
+#!/usr/bin/env bash
+#
+# vim:ts=5:sw=5:expandtab
+# we have a spaces softtab, that ensures readability with other editors too
+
+# testssl.sh is a program for spotting weak SSL/TLS encryption, ciphers, protocols and some
+# vulnerabilities or features. It may or may be not distributed by your distribution.
+# The upstream versions are available (please leave the links intact):
+#
+# Development version https://github.com/drwetter/testssl.sh
+# Stable version https://testssl.sh
+# File bugs at github https://github.com/drwetter/testssl.sh/issues
+#
+# Project lead and initiator: Dirk Wetter, copyleft: 2007-today.
+# Main contributions from David Cooper. Further contributors see CREDITS.md .
+#
+# License: GPLv2, see https://www.fsf.org/licensing/licenses/info/GPLv2.html
+# and accompanying license "LICENSE.txt". Redistribution + modification under this
+# license permitted.
+# If you enclose this program or parts of it in your software, it has to be
+# accompanied by the same license (see link). Do not violate the license.
+# If you do not agree to these terms, do not use it in the first place!
+#
+# OpenSSL, which is being used and maybe distributed via one of this projects'
+# web sites, is subject to their licensing: https://www.openssl.org/source/license.txt
+#
+# The client simulation data comes from SSLlabs and is licensed to the 'Qualys SSL Labs
+# Terms of Use' (v2.2), see https://www.ssllabs.com/downloads/Qualys_SSL_Labs_Terms_of_Use.pdf,
+# stating a CC BY 3.0 US license: https://creativecommons.org/licenses/by/3.0/us/
+#
+# Please note: USAGE WITHOUT ANY WARRANTY, THE SOFTWARE IS PROVIDED "AS IS".
+# USE IT AT your OWN RISK!
+# Seriously! The threat is you run this code on your computer and untrusted input e.g.
+# could be supplied from a server you are querying.
+#
+# HISTORY:
+# Back in 2006 it all started with a few openssl commands...
+# That's because openssl is a such a good swiss army knife (see e.g.
+# https://wiki.openssl.org/index.php/Command_Line_Utilities) that it was difficult to resist
+# wrapping some shell commands around it, which I used for my pen tests. This is how
+# everything started.
+# Now it has grown up, it has bash socket support for most features, which has been basically
+# replacing more and more functions of OpenSSL and some sockets functions serve as some kind
+# of central functions.
+#
+# WHY BASH?
+# Cross-platform is one of the three main goals of this script. Second: Ease of installation.
+# No compiling, install gems, go to CPAN, use pip etc. Third: Easy to use and to interpret
+# the results.
+# /bin/bash including the builtin sockets fulfill all that. The socket checks in bash may sound
+# cool and unique -- they are -- but probably you can achieve e.g. the same result with my favorite
+# interactive shell: zsh (zmodload zsh/net/socket -- checkout zsh/net/tcp) too! Oh, and btw.
+# ksh93 has socket support too.
+# /bin/bash though is way more often used within Linux and it's perfect for cross platform support.
+# MacOS X has it and also under Windows the MSYS2 extension or Cygwin as well as Bash on Windows (WSL)
+# has /bin/bash.
+#
+# Q: So what's the difference to www.ssllabs.com/ssltest/ or sslcheck.globalsign.com/ ?
+# A: As of now ssllabs only check 1) webservers 2) on standard ports, 3) reachable from the
+# internet. And those examples above 4) are 3rd parties. If these restrictions are all fine
+# with you and you need a management compatible rating -- go ahead and use those.
+#
+# But also if your fine with those restrictions: testssl.sh is meant as a tool in your hand
+# and it's way more flexible. Oh, and did I mention testssl.sh is open source?
+#
+#################### Stop talking, action now ####################
+
+
+########### Definition of error codes
+#
+declare -r ERR_BASH=255 # Bash version incorrect
+declare -r ERR_CMDLINE=254 # Cmd line couldn't be parsed
+declare -r ERR_FCREATE=253 # Output file couldn't be created
+declare -r ERR_FNAMEPARSE=252 # Input file couldn't be parsed
+declare -r ERR_NOSUPPORT=251 # Feature requested is not supported
+declare -r ERR_OSSLBIN=250 # Problem with OpenSSL binary
+declare -r ERR_DNSBIN=249 # Problem with DNS lookup binaries
+declare -r ERR_OTHERCLIENT=248 # Other client problem
+declare -r ERR_DNSLOOKUP=247 # Problem with resolving IP addresses or names
+declare -r ERR_CONNECT=246 # Connectivity problem
+declare -r ERR_CLUELESS=245 # Weird state, either though user options or testssl.sh
+declare -r ERR_RESOURCE=244 # Resources testssl.sh needs couldn't be read
+declare -r ERR_CHILD=242 # Child received a signal from master
+declare -r ALLOK=0 # All is fine
+
+
+[ -z "${BASH_VERSINFO[0]}" ] && printf "\n\033[1;35m Please make sure you're using \"bash\"! Bye...\033[m\n\n" >&2 && exit $ERR_BASH
+[ $(kill -l | grep -c SIG) -eq 0 ] && printf "\n\033[1;35m Please make sure you're calling me without leading \"sh\"! Bye...\033[m\n\n" >&2 && exit $ERR_BASH
+[ ${BASH_VERSINFO[0]} -lt 3 ] && printf "\n\033[1;35m Minimum requirement is bash 3.2. You have $BASH_VERSION \033[m\n\n" >&2 && exit $ERR_BASH
+[ ${BASH_VERSINFO[0]} -le 3 ] && [ ${BASH_VERSINFO[1]} -le 1 ] && printf "\n\033[1;35m Minimum requirement is bash 3.2. You have $BASH_VERSION \033[m\n\n" >&2 && exit $ERR_BASH
+
+########### Debugging helpers + profiling
+#
+declare -r PS4='|${LINENO}> \011${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
+DEBUGTIME=${DEBUGTIME:-false} # https://stackoverflow.com/questions/5014823/how-to-profile-a-bash-shell-script-slow-startup#20855353, profiling bash
+DEBUG_ALLINONE=${DEBUG_ALLINONE:-false} # true: do debugging in one screen (old behavior for testssl.sh and bash3's default
+ # false: needed for performance analysis or useful for just having an extra file
+DEBUG_ALLINONE=${SETX:-false} # SETX as a shortcut for old style debugging, overriding DEBUG_ALLINONE
+if [[ "$SHELLOPTS" =~ xtrace ]]; then
+ if "$DEBUGTIME"; then
+ # separate debugging, doesn't mess up the screen, $DEBUGTIME determines whether we also do performance analysis
+ exec 42>&2 2> >(tee /tmp/testssl-$$.log | sed -u 's/^.*$/now/' | date -f - +%s.%N >/tmp/testssl-$$.time)
+ # BASH_XTRACEFD=42
+ else
+ if ! "$DEBUG_ALLINONE"; then
+ exec 42>| /tmp/testssl-$$.log
+ BASH_XTRACEFD=42
+ fi
+ fi
+fi
+
+########### Traps! Make sure that temporary files are cleaned up after use in ANY case
+#
+trap "cleanup" QUIT EXIT
+trap "child_error" USR1
+
+
+########### Internal definitions
+#
+declare -r VERSION="3.0.6"
+declare -r SWCONTACT="dirk aet testssl dot sh"
+grep -E -q "dev|rc|beta" <<< "$VERSION" && \
+ SWURL="https://testssl.sh/dev/" ||
+ SWURL="https://testssl.sh/"
+if git log &>/dev/null; then
+ declare -r GIT_REL="$(git log --format='%h %ci' -1 2>/dev/null | awk '{ print $1" "$2" "$3 }')"
+ declare -r GIT_REL_SHORT="$(git log --format='%h %ci' -1 2>/dev/null | awk '{ print $1 }')"
+ declare -r REL_DATE="$(git log --format='%h %ci' -1 2>/dev/null | awk '{ print $2 }')"
+fi
+declare -r PROG_NAME="$(basename "$0")"
+declare -r RUN_DIR="$(dirname "$0")"
+declare -r SYSTEM="$(uname -s)"
+declare -r SYSTEMREV="$(uname -r)"
+SYSTEM2="" # currently only being used for WSL = bash on windows
+TESTSSL_INSTALL_DIR="${TESTSSL_INSTALL_DIR:-""}" # If you run testssl.sh and it doesn't find it necessary file automagically set TESTSSL_INSTALL_DIR
+CA_BUNDLES_PATH="${CA_BUNDLES_PATH:-""}" # You can have your stores some place else
+ADDITIONAL_CA_FILES="${ADDITIONAL_CA_FILES:-""}" # single file with a CA in PEM format or comma separated lists of them
+CIPHERS_BY_STRENGTH_FILE=""
+TLS_DATA_FILE="" # mandatory file for socket-based handshakes
+OPENSSL_LOCATION=""
+HNAME="$(uname -n)"
+HNAME="${HNAME%%.*}"
+
+declare CMDLINE
+CMDLINE_PARSED="" # This makes sure we don't let early fatal() write into files when files aren't created yet
+declare -r -a CMDLINE_ARRAY=("$@") # When performing mass testing, the child processes need to be sent the
+declare -a MASS_TESTING_CMDLINE # command line in the form of an array (see #702 and https://mywiki.wooledge.org/BashFAQ/050).
+
+
+########### Some predefinitions: date, sed (we always use test and NOT try to determine
+# capabilities by querying the OS)
+#
+HAS_GNUDATE=false
+HAS_FREEBSDDATE=false
+HAS_OPENBSDDATE=false
+if date -d @735275209 >/dev/null 2>&1; then
+ if date -r @735275209 >/dev/null 2>&1; then
+ # It can't do any conversion from a plain date output.
+ HAS_OPENBSDDATE=true
+ else
+ HAS_GNUDATE=true
+ fi
+fi
+# FreeBSD and OS X date(1) accept "-f inputformat", so do newer OpenBSD versions >~ 6.6.
+date -j -f '%s' 1234567 >/dev/null 2>&1 && \
+ HAS_FREEBSDDATE=true
+
+echo A | sed -E 's/A//' >/dev/null 2>&1 && \
+ declare -r HAS_SED_E=true || \
+ declare -r HAS_SED_E=false
+
+########### Terminal definitions
+tty -s && \
+ declare -r INTERACTIVE=true || \
+ declare -r INTERACTIVE=false
+
+if [[ -z $TERM_WIDTH ]]; then # no batch file and no otherwise predefined TERM_WIDTH
+ if ! tput cols &>/dev/null || ! "$INTERACTIVE";then # Prevent tput errors if running non interactive
+ export TERM_WIDTH=${COLUMNS:-80}
+ else
+ export TERM_WIDTH=${COLUMNS:-$(tput cols)} # for custom line wrapping and dashes
+ fi
+fi
+TERM_CURRPOS=0 # custom line wrapping needs alter the current horizontal cursor pos
+
+
+########### Defining (and presetting) variables which can be changed
+#
+# Following variables make use of $ENV and can be used like "OPENSSL=<myprivate_path_to_openssl> ./testssl.sh <URI>"
+declare -x OPENSSL
+OPENSSL_TIMEOUT=${OPENSSL_TIMEOUT:-""} # Default connect timeout with openssl before we call the server side unreachable
+CONNECT_TIMEOUT=${CONNECT_TIMEOUT:-""} # Default connect timeout with sockets before we call the server side unreachable
+PHONE_OUT=${PHONE_OUT:-false} # Whether testssl can retrieve CRLs and OCSP
+FAST_SOCKET=${FAST_SOCKET:-false} # EXPERIMENTAL feature to accelerate sockets -- DO NOT USE it for production
+COLOR=${COLOR:-2} # 3: Extra color (ciphers, curves), 2: Full color, 1: B/W only 0: No ESC at all
+COLORBLIND=${COLORBLIND:-false} # if true, swap blue and green in the output
+SHOW_EACH_C=${SHOW_EACH_C:-false} # where individual ciphers are tested show just the positively ones tested
+SHOW_SIGALGO=${SHOW_SIGALGO:-false} # "secret" switch whether testssl.sh shows the signature algorithm for -E / -e
+SNEAKY=${SNEAKY:-false} # is the referer and useragent we leave behind just usual?
+QUIET=${QUIET:-false} # don't output the banner. By doing this you acknowledge usage term appearing in the banner
+SSL_NATIVE=${SSL_NATIVE:-false} # we do per default bash sockets where possible "true": switch back to "openssl native"
+ASSUME_HTTP=${ASSUME_HTTP:-false} # in seldom cases (WAF, old servers, grumpy SSL) service detection fails. "True" enforces HTTP checks
+BASICAUTH=${BASICAUTH:-""} # HTTP basic auth credentials can be set here like user:pass
+BUGS=${BUGS:-""} # -bugs option from openssl, needed for some BIG IP F5
+WARNINGS=${WARNINGS:-""} # can be either off or batch
+DEBUG=${DEBUG:-0} # 1: normal output the files in /tmp/ are kept for further debugging purposes
+ # 2: list more what's going on , also lists some errors of connections
+ # 3: slight hexdumps + other info,
+ # 4: display bytes sent via sockets
+ # 5: display bytes received via sockets
+ # 6: whole 9 yards
+FAST=${FAST:-false} # preference: show only first cipher, run_allciphers with openssl instead of sockets
+WIDE=${WIDE:-false} # whether to display for some options just ciphers or a table w hexcode/KX,Enc,strength etc.
+MASS_TESTING_MODE=${MASS_TESTING_MODE:-serial} # can be serial or parallel. Subject to change
+LOGFILE="${LOGFILE:-""}" # logfile if used
+JSONFILE="${JSONFILE:-""}" # jsonfile if used
+CSVFILE="${CSVFILE:-""}" # csvfile if used
+HTMLFILE="${HTMLFILE:-""}" # HTML if used
+FNAME=${FNAME:-""} # file name to read commands from
+FNAME_PREFIX=${FNAME_PREFIX:-""} # output filename prefix, see --outprefix
+APPEND=${APPEND:-false} # append to csv/json file instead of overwriting it
+[[ -z "$NODNS" ]] && declare NODNS # If unset it does all DNS lookups per default. "min" only for hosts or "none" at all
+HAS_IPv6=${HAS_IPv6:-false} # if you have OpenSSL with IPv6 support AND IPv6 networking set it to yes
+ALL_CLIENTS=${ALL_CLIENTS:-false} # do you want to run all client simulation form all clients supplied by SSLlabs?
+OFFENSIVE=${OFFENSIVE:-true} # do you want to include offensive vulnerability tests which may cause blocking by an IDS?
+
+########### Tuning vars which cannot be set by a cmd line switch. Use instead e.g "HEADER_MAXSLEEP=10 ./testssl.sh <your_args_here>"
+#
+EXPERIMENTAL=${EXPERIMENTAL:-false} # a development hook which allows us to disable code
+PROXY_WAIT=${PROXY_WAIT:-20} # waiting at max 20 seconds for socket reply through proxy
+DNS_VIA_PROXY=${DNS_VIA_PROXY:-true} # do DNS lookups via proxy. --ip=proxy reverses this
+IGN_OCSP_PROXY=${IGN_OCSP_PROXY:-false} # Also when --proxy is supplied it is ignored when testing for revocation via OCSP via --phone-out
+HEADER_MAXSLEEP=${HEADER_MAXSLEEP:-5} # we wait this long before killing the process to retrieve a service banner / http header
+MAX_SOCKET_FAIL=${MAX_SOCKET_FAIL:-2} # If this many failures for TCP socket connects are reached we terminate
+MAX_OSSL_FAIL=${MAX_OSSL_FAIL:-2} # If this many failures for s_client connects are reached we terminate
+MAX_HEADER_FAIL=${MAX_HEADER_FAIL:-2} # If this many failures for HTTP GET are encountered we don't try again to get the header
+MAX_WAITSOCK=${MAX_WAITSOCK:-10} # waiting at max 10 seconds for socket reply. There shouldn't be any reason to change this.
+CCS_MAX_WAITSOCK=${CCS_MAX_WAITSOCK:-5} # for the two CCS payload (each). There shouldn't be any reason to change this.
+HEARTBLEED_MAX_WAITSOCK=${HEARTBLEED_MAX_WAITSOCK:-8} # for the heartbleed payload. There shouldn't be any reason to change this.
+STARTTLS_SLEEP=${STARTTLS_SLEEP:-10} # max time wait on a socket for STARTTLS. MySQL has a fixed value of 1 which can't be overwritten (#914)
+FAST_STARTTLS=${FAST_STARTTLS:-true} # at the cost of reliability decrease the handshakes for STARTTLS
+USLEEP_SND=${USLEEP_SND:-0.1} # sleep time for general socket send
+USLEEP_REC=${USLEEP_REC:-0.2} # sleep time for general socket receive
+HSTS_MIN=${HSTS_MIN:-180} # >=180 days is ok for HSTS
+ HSTS_MIN=$((HSTS_MIN * 86400)) # correct to seconds
+HPKP_MIN=${HPKP_MIN:-30} # >=30 days should be ok for HPKP_MIN, practical hints?
+ HPKP_MIN=$((HPKP_MIN * 86400)) # correct to seconds
+DAYS2WARN1=${DAYS2WARN1:-60} # days to warn before cert expires, threshold 1
+DAYS2WARN2=${DAYS2WARN2:-30} # days to warn before cert expires, threshold 2
+VULN_THRESHLD=${VULN_THRESHLD:-1} # if vulnerabilities to check >$VULN_THRESHLD we DON'T show a separate header line in the output each vuln. check
+UNBRACKTD_IPV6=${UNBRACKTD_IPV6:-false} # some versions of OpenSSL (like Gentoo) don't support [bracketed] IPv6 addresses
+NO_ENGINE=${NO_ENGINE:-false} # if there are problems finding the (external) openssl engine set this to true
+declare -r CLIENT_MIN_PFS=5 # number of ciphers needed to run a test for PFS
+CAPATH="${CAPATH:-/etc/ssl/certs/}" # Does nothing yet (FC has only a CA bundle per default, ==> openssl version -d)
+GOOD_CA_BUNDLE="" # A bundle of CA certificates that can be used to validate the server's certificate
+CERTIFICATE_LIST_ORDERING_PROBLEM=false # Set to true if server sends a certificate list that contains a certificate
+ # that does not certify the one immediately preceding it. (See RFC 8446, Section 4.4.2)
+STAPLED_OCSP_RESPONSE=""
+HAS_DNS_SANS=false # Whether the certificate includes a subjectAltName extension with a DNS name or an application-specific identifier type.
+MEASURE_TIME_FILE=${MEASURE_TIME_FILE:-""}
+if [[ -n "$MEASURE_TIME_FILE" ]] && [[ -z "$MEASURE_TIME" ]]; then
+ MEASURE_TIME=true
+else
+ MEASURE_TIME=${MEASURE_TIME:-false}
+fi
+DISPLAY_CIPHERNAMES="openssl" # display OpenSSL ciphername (but both OpenSSL and RFC ciphernames in wide mode)
+declare -r UA_STD="TLS tester from $SWURL"
+declare -r UA_SNEAKY="Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0"
+
+########### Initialization part, further global vars just being declared here
+#
+LC_COLLATE=en_US.UTF-8 # ensures certain regex patterns work as expected and aren't localized, see #1860
+PRINTF="" # which external printf to use. Empty presets the internal one, see #1130
+IKNOW_FNAME=false
+FIRST_FINDING=true # is this the first finding we are outputting to file?
+JSONHEADER=true # include JSON headers and footers in HTML file, if one is being created
+CSVHEADER=true # same for CSV
+HTMLHEADER=true # same for HTML
+SECTION_FOOTER_NEEDED=false # kludge for tracking whether we need to close the JSON section object
+GIVE_HINTS=false # give an additional info to findings
+SERVER_SIZE_LIMIT_BUG=false # Some servers have either a ClientHello total size limit or a 128 cipher limit (e.g. old ASAs)
+MULTIPLE_CHECKS=false # need to know whether an MX record or a hostname resolves to multiple IPs to check
+CHILD_MASS_TESTING=${CHILD_MASS_TESTING:-false}
+TIMEOUT_CMD=""
+HAD_SLEPT=0
+NR_SOCKET_FAIL=0 # Counter for socket failures
+NR_OSSL_FAIL=0 # .. for OpenSSL connects
+NR_HEADER_FAIL=0 # .. for HTTP_GET
+PROTOS_OFFERED="" # This keeps which protocol is being offered. See has_server_protocol().
+TLS12_CIPHER_OFFERED="" # This contains the hexcode of a cipher known to be supported by the server with TLS 1.2
+CURVES_OFFERED="" # This keeps which curves have been detected. Just for error handling
+KNOWN_OSSL_PROB=false # We need OpenSSL a few times. This variable is an indicator if we can't connect. Eases handling
+DETECTED_TLS_VERSION="" # .. as hex string, e.g. 0300 or 0303
+TLS13_ONLY=false # Does the server support TLS 1.3 ONLY?
+OSSL_SHORTCUT=${OSSL_SHORTCUT:-false} # Hack: if during the scan turns out the OpenSSL binary supports TLS 1.3 would be a better choice, this enables it.
+TLS_EXTENSIONS=""
+declare -r NPN_PROTOs="spdy/4a2,spdy/3,spdy/3.1,spdy/2,spdy/1,http/1.1"
+# alpn_protos needs to be space-separated, not comma-seperated, including odd ones observed @ facebook and others, old ones like h2-17 omitted as they could not be found
+declare -r ALPN_PROTOs="h2 spdy/3.1 http/1.1 grpc-exp h2-fb spdy/1 spdy/2 spdy/3 stun.turn stun.nat-discovery webrtc c-webrtc ftp"
+declare -a SESS_RESUMPTION
+TEMPDIR=""
+TMPFILE=""
+ERRFILE=""
+CLIENT_AUTH=false
+TLS_TICKETS=false
+NO_SSL_SESSIONID=false
+CERT_COMPRESSION=${CERT_COMPRESSION:-false} # secret flag to set in addition to --devel for certificate compression
+HOSTCERT="" # File with host certificate, without intermediate certificate
+HEADERFILE=""
+HEADERVALUE=""
+HTTP_STATUS_CODE=""
+DH_GROUP_OFFERED=""
+DH_GROUP_LEN_P=0
+KEY_SHARE_EXTN_NR="33" # The extension number for key_share was changed from 40 to 51 in TLSv1.3 draft 23.
+ # In order to support draft 23 and later in addition to earlier drafts, need to
+ # know which extension number to use. Note that it appears that a single
+ # ClientHello cannot advertise both draft 23 and later and earlier drafts.
+ # Preset may help to deal with STARTTLS + TLS 1.3 draft 23 and later but not earlier.
+BAD_SERVER_HELLO_CIPHER=false # reserved for cases where a ServerHello doesn't contain a cipher offered in the ClientHello
+GOST_STATUS_PROBLEM=false
+PATTERN2SHOW=""
+SOCK_REPLY_FILE=""
+NW_STR=""
+LEN_STR=""
+SNI=""
+POODLE="" # keep vulnerability status for TLS_FALLBACK_SCSV
+OSSL_NAME="" # openssl name, in case of LibreSSL it's LibreSSL
+OSSL_VER="" # openssl version, will be auto-determined
+OSSL_VER_MAJOR=0
+OSSL_VER_MINOR=0
+OSSL_VER_APPENDIX="none"
+CLIENT_PROB_NO=1
+HAS_DH_BITS=${HAS_DH_BITS:-false} # initialize openssl variables
+HAS_CURVES=false
+OSSL_SUPPORTED_CURVES=""
+HAS_SSL2=false
+HAS_SSL3=false
+HAS_TLS13=false
+HAS_X448=false
+HAS_X25519=false
+HAS_PKUTIL=false
+HAS_PKEY=false
+HAS_NO_SSL2=false
+HAS_NOSERVERNAME=false
+HAS_CIPHERSUITES=false
+HAS_COMP=false
+HAS_NO_COMP=false
+HAS_ALPN=false
+HAS_NPN=false
+HAS_FALLBACK_SCSV=false
+HAS_PROXY=false
+HAS_XMPP=false
+HAS_POSTGRES=false
+HAS_MYSQL=false
+HAS_LMTP=false
+HAS_NNTP=false
+HAS_IRC=false
+HAS_CHACHA20=false
+HAS_AES128_GCM=false
+HAS_AES256_GCM=false
+HAS_ZLIB=false
+HAS_DIG=false
+HAS_DIG_R=true
+DIG_R="-r"
+HAS_HOST=false
+HAS_DRILL=false
+HAS_NSLOOKUP=false
+HAS_IDN=false
+HAS_IDN2=false
+HAS_AVAHIRESOLVE=false
+HAS_DIG_NOIDNOUT=false
+
+OSSL_CIPHERS_S=""
+PORT=443 # unless otherwise auto-determined, see below
+NODE=""
+NODEIP=""
+rDNS=""
+CORRECT_SPACES="" # Used for IPv6 and proper output formatting
+IPADDRs=""
+IP46ADDRs=""
+LOCAL_A=false # Does the $NODEIP come from /etc/hosts?
+LOCAL_AAAA=false # Does the IPv6 IP come from /etc/hosts?
+XMPP_HOST=""
+PROXYIP="" # $PROXYIP:$PROXPORT is your proxy if --proxy is defined ...
+PROXYPORT="" # ... and openssl has proxy support
+PROXY="" # Once check_proxy() executed it contains $PROXYIP:$PROXPORT
+VULN_COUNT=0
+SERVICE="" # Is the server running an HTTP server, SMTP, POP or IMAP?
+URI=""
+CERT_FINGERPRINT_SHA2=""
+RSA_CERT_FINGERPRINT_SHA2=""
+STARTTLS_PROTOCOL=""
+OPTIMAL_PROTO="" # Need this for IIS6 (sigh) + OpenSSL 1.0.2, otherwise some handshakes will fail see
+ # https://github.com/PeterMosmans/openssl/issues/19#issuecomment-100897892
+STARTTLS_OPTIMAL_PROTO="" # Same for STARTTLS, see https://github.com/drwetter/testssl.sh/issues/188
+OPTIMAL_SOCKETS_PROTO="" # Same for tls_sockets(). -- not yet used
+ALL_FAILED_SOCKETS=true # Set to true if all attempts to connect to server using tls_sockets/sslv2_sockets failed
+TLS_TIME="" # To keep the value of TLS server timestamp
+TLS_NOW="" # Similar
+TLS_DIFFTIME_SET=false # Tells TLS functions to measure the TLS difftime or not
+NOW_TIME=""
+HTTP_TIME=""
+GET_REQ11=""
+START_TIME=0 # time in epoch when the action started
+END_TIME=0 # .. ended
+SCAN_TIME=0 # diff of both: total scan time
+LAST_TIME=0 # only used for performance measurements (MEASURE_TIME=true)
+SERVER_COUNTER=0 # Counter for multiple servers
+
+TLS_LOW_BYTE="" # For "secret" development stuff, see -q below
+HEX_CIPHER="" # "
+
+
+########### Global variables for parallel mass testing
+#
+declare -r PARALLEL_SLEEP=1 # Time to sleep after starting each test
+MAX_WAIT_TEST=${MAX_WAIT_TEST:-1200} # Maximum time (in seconds) to wait for a test to complete
+MAX_PARALLEL=${MAX_PARALLEL:-20} # Maximum number of tests to run in parallel
+ # This value may be made larger on systems with faster processors
+declare -a -i PARALLEL_TESTING_PID=() # process id for each child test (or 0 to indicate test has already completed)
+declare -a PARALLEL_TESTING_CMDLINE=() # command line for each child test
+declare -i NR_PARALLEL_TESTS=0 # number of parallel tests run
+declare -i NEXT_PARALLEL_TEST_TO_FINISH=0 # number of parallel tests that have completed and have been processed
+declare FIRST_JSON_OUTPUT=true # true if no output has been added to $JSONFILE yet.
+
+
+########### Cipher suite information
+#
+declare -i TLS_NR_CIPHERS=0
+declare TLS_CIPHER_HEXCODE=()
+declare TLS_CIPHER_OSSL_NAME=()
+declare TLS_CIPHER_RFC_NAME=()
+declare TLS_CIPHER_SSLVERS=()
+declare TLS_CIPHER_KX=()
+declare TLS_CIPHER_AUTH=()
+declare TLS_CIPHER_ENC=()
+declare TLS_CIPHER_EXPORT=()
+declare TLS_CIPHER_OSSL_SUPPORTED=()
+declare TLS13_OSSL_CIPHERS="TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_CCM_SHA256:TLS_AES_128_CCM_8_SHA256"
+
+########### Severity functions and globals
+#
+INFO=0
+OK=0
+LOW=1
+MEDIUM=2
+HIGH=3
+CRITICAL=4
+SEVERITY_LEVEL=0
+
+set_severity_level() {
+ local severity=$1
+
+ if [[ "$severity" == LOW ]]; then
+ SEVERITY_LEVEL=$LOW
+ elif [[ "$severity" == MEDIUM ]]; then
+ SEVERITY_LEVEL=$MEDIUM
+ elif [[ "$severity" == HIGH ]]; then
+ SEVERITY_LEVEL=$HIGH
+ elif [[ "$severity" == CRITICAL ]]; then
+ SEVERITY_LEVEL=$CRITICAL
+ else
+ # WARN and FATAL will always be logged as the represent scanning problems
+ echo "Supported severity levels are LOW, MEDIUM, HIGH, CRITICAL!"
+ help 1
+ fi
+}
+
+show_finding() {
+ local severity=$1
+
+ ( [[ "$severity" == DEBUG ]] ) ||
+ ( [[ "$severity" == INFO ]] && [[ $SEVERITY_LEVEL -le $INFO ]] ) ||
+ ( [[ "$severity" == OK ]] && [[ $SEVERITY_LEVEL -le $OK ]] ) ||
+ ( [[ "$severity" == LOW ]] && [[ $SEVERITY_LEVEL -le $LOW ]] ) ||
+ ( [[ "$severity" == MEDIUM ]] && [[ $SEVERITY_LEVEL -le $MEDIUM ]] ) ||
+ ( [[ "$severity" == HIGH ]] && [[ $SEVERITY_LEVEL -le $HIGH ]] ) ||
+ ( [[ "$severity" == CRITICAL ]] && [[ $SEVERITY_LEVEL -le $CRITICAL ]] ) ||
+ ( [[ "$severity" == WARN ]] ) ||
+ ( [[ "$severity" == FATAL ]] )
+}
+
+########### Output functions
+
+# For HTML output, replace any HTML reserved characters with the entity name
+html_reserved(){
+ local output
+ "$do_html" || return 0
+ #sed -e 's/\&/\&amp;/g' -e 's/</\&lt;/g' -e 's/>/\&gt;/g' -e 's/"/\&quot;/g' -e "s/'/\&apos;/g" <<< "$1"
+ output="${1//&/&amp;}"
+ output="${output//</&lt;}"
+ output="${output//>/&gt;}"
+ output="${output//\"/&quot;}"
+ output="${output//\'/&apos;}"
+ printf -- "%s" "$output"
+ return 0
+}
+
+html_out() {
+ "$do_html" || return 0
+ [[ -n "$HTMLFILE" ]] && [[ ! -d "$HTMLFILE" ]] && printf -- "%b" "$1" >> "$HTMLFILE"
+}
+
+# This is intentionally the same.
+safe_echo() { printf -- "%b" "$1"; }
+tm_out() { printf -- "%b" "$1"; }
+tmln_out() { printf -- "%b" "$1\n"; }
+
+out() { printf -- "%b" "$1"; html_out "$(html_reserved "$1")"; }
+outln() { printf -- "%b" "$1\n"; html_out "$(html_reserved "$1")\n"; }
+
+
+#TODO: Still no shell injection safe but if just run it from the cmd line: that's fine
+
+# Color print functions, see also https://www.tldp.org/HOWTO/Bash-Prompt-HOWTO/x329.html
+tm_liteblue() { [[ "$COLOR" -ge 2 ]] && ( "$COLORBLIND" && tm_out "\033[0;32m$1" || tm_out "\033[0;34m$1" ) || tm_out "$1"; tm_off; } # not yet used
+pr_liteblue() { tm_liteblue "$1"; [[ "$COLOR" -ge 2 ]] && ( "$COLORBLIND" && html_out "<span style=\"color:#00cd00;\">$(html_reserved "$1")</span>" || html_out "<span style=\"color:#0000ee;\">$(html_reserved "$1")</span>" ) || html_out "$(html_reserved "$1")"; }
+tmln_liteblue() { tm_liteblue "$1"; tmln_out; }
+prln_liteblue() { pr_liteblue "$1"; outln; }
+
+tm_blue() { [[ "$COLOR" -ge 2 ]] && ( "$COLORBLIND" && tm_out "\033[1;32m$1" || tm_out "\033[1;34m$1" ) || tm_out "$1"; tm_off; } # used for head lines of single tests
+pr_blue() { tm_blue "$1"; [[ "$COLOR" -ge 2 ]] && ( "$COLORBLIND" && html_out "<span style=\"color:lime;font-weight:bold;\">$(html_reserved "$1")</span>" || html_out "<span style=\"color:#5c5cff;font-weight:bold;\">$(html_reserved "$1")</span>" ) || html_out "$(html_reserved "$1")"; }
+tmln_blue() { tm_blue "$1"; tmln_out; }
+prln_blue() { pr_blue "$1"; outln; }
+
+# we should be able to use aliases here
+tm_warning() { [[ "$COLOR" -ge 2 ]] && tm_out "\033[0;35m$1" || tm_underline "$1"; tm_off; } # some local problem: one test cannot be done
+tmln_warning() { tm_warning "$1"; tmln_out; } # litemagenta
+pr_warning() { tm_warning "$1"; [[ "$COLOR" -ge 2 ]] && html_out "<span style=\"color:#cd00cd;\">$(html_reserved "$1")</span>" || ( [[ "$COLOR" -eq 1 ]] && html_out "<u>$(html_reserved "$1")</u>" || html_out "$(html_reserved "$1")" ); }
+prln_warning() { pr_warning "$1"; outln; }
+
+tm_magenta() { [[ "$COLOR" -ge 2 ]] && tm_out "\033[1;35m$1" || tm_underline "$1"; tm_off; } # fatal error: quitting because of this!
+tmln_magenta() { tm_magenta "$1"; tmln_out; }
+# different as warning above?
+pr_magenta() { tm_magenta "$1"; [[ "$COLOR" -ge 2 ]] && html_out "<span style=\"color:magenta;font-weight:bold;\">$(html_reserved "$1")</span>" || ( [[ "$COLOR" -eq 1 ]] && html_out "<u>$(html_reserved "$1")</u>" || html_out "$(html_reserved "$1")" ); }
+prln_magenta() { pr_magenta "$1"; outln; }
+
+tm_litecyan() { [[ "$COLOR" -ge 2 ]] && tm_out "\033[0;36m$1" || tm_out "$1"; tm_off; } # not yet used
+tmln_litecyan() { tm_litecyan "$1"; tmln_out; }
+pr_litecyan() { tm_litecyan "$1"; [[ "$COLOR" -ge 2 ]] && html_out "<span style=\"color:#00cdcd;\">$(html_reserved "$1")</span>" || html_out "$(html_reserved "$1")"; }
+prln_litecyan() { pr_litecyan "$1"; outln; }
+
+tm_cyan() { [[ "$COLOR" -ge 2 ]] && tm_out "\033[1;36m$1" || tm_out "$1"; tm_off; } # additional hint
+tmln_cyan() { tm_cyan "$1"; tmln_out; }
+pr_cyan() { tm_cyan "$1"; [[ "$COLOR" -ge 2 ]] && html_out "<span style=\"color:cyan;font-weight:bold;\">$(html_reserved "$1")</span>" || html_out "$(html_reserved "$1")"; }
+prln_cyan() { pr_cyan "$1"; outln; }
+
+tm_litegrey() { [[ "$COLOR" -ne 0 ]] && tm_out "\033[0;37m$1" || tm_out "$1"; tm_off; } # ... https://github.com/drwetter/testssl.sh/pull/600#issuecomment-276129876
+tmln_litegrey() { tm_litegrey "$1"; tmln_out; } # not really usable on a black background, see ..
+prln_litegrey() { pr_litegrey "$1"; outln; }
+pr_litegrey() { tm_litegrey "$1"; [[ "$COLOR" -ne 0 ]] && html_out "<span style=\"color:darkgray;\">$(html_reserved "$1")</span>" || html_out "$(html_reserved "$1")"; }
+
+tm_grey() { [[ "$COLOR" -ne 0 ]] && tm_out "\033[1;30m$1" || tm_out "$1"; tm_off; }
+pr_grey() { tm_grey "$1"; [[ "$COLOR" -ne 0 ]] && html_out "<span style=\"color:#7f7f7f;font-weight:bold;\">$(html_reserved "$1")</span>" || html_out "$(html_reserved "$1")"; }
+tmln_grey() { tm_grey "$1"; tmln_out; }
+prln_grey() { pr_grey "$1"; outln; }
+
+tm_svrty_good() { [[ "$COLOR" -ge 2 ]] && ( "$COLORBLIND" && tm_out "\033[0;34m$1" || tm_out "\033[0;32m$1" ) || tm_out "$1"; tm_off; } # litegreen (liteblue), This is good
+tmln_svrty_good() { tm_svrty_good "$1"; tmln_out; }
+pr_svrty_good() { tm_svrty_good "$1"; [[ "$COLOR" -ge 2 ]] && ( "$COLORBLIND" && html_out "<span style=\"color:#0000ee;\">$(html_reserved "$1")</span>" || html_out "<span style=\"color:#00cd00;\">$(html_reserved "$1")</span>" ) || html_out "$(html_reserved "$1")"; }
+prln_svrty_good() { pr_svrty_good "$1"; outln; }
+
+tm_svrty_best() { [[ "$COLOR" -ge 2 ]] && ( "$COLORBLIND" && tm_out "\033[1;34m$1" || tm_out "\033[1;32m$1" ) || tm_out "$1"; tm_off; } # green (blue), This is the best
+tmln_svrty_best() { tm_svrty_best "$1"; tmln_out; }
+pr_svrty_best() { tm_svrty_best "$1"; [[ "$COLOR" -ge 2 ]] && ( "$COLORBLIND" && html_out "<span style=\"color:#5c5cff;font-weight:bold;\">$(html_reserved "$1")</span>" || html_out "<span style=\"color:lime;font-weight:bold;\">$(html_reserved "$1")</span>" ) || html_out "$(html_reserved "$1")"; }
+prln_svrty_best() { pr_svrty_best "$1"; outln; }
+
+tm_svrty_low() { [[ "$COLOR" -ge 2 ]] && tm_out "\033[1;33m$1" || tm_out "$1"; tm_off; } # yellow brown | academic or minor problem
+tmln_svrty_low() { tm_svrty_low "$1"; tmln_out; }
+pr_svrty_low() { tm_svrty_low "$1"; [[ "$COLOR" -ge 2 ]] && html_out "<span style=\"color:#cdcd00;font-weight:bold;\">$(html_reserved "$1")</span>" || html_out "$(html_reserved "$1")"; }
+prln_svrty_low() { pr_svrty_low "$1"; outln; }
+
+tm_svrty_medium() { [[ "$COLOR" -ge 2 ]] && tm_out "\033[0;33m$1" || tm_out "$1"; tm_off; } # brown | it is not a bad problem but you shouldn't do this
+pr_svrty_medium() { tm_svrty_medium "$1"; [[ "$COLOR" -ge 2 ]] && html_out "<span style=\"color:#cd8000;\">$(html_reserved "$1")</span>" || html_out "$(html_reserved "$1")"; }
+tmln_svrty_medium(){ tm_svrty_medium "$1"; tmln_out; }
+prln_svrty_medium(){ pr_svrty_medium "$1"; outln; }
+
+tm_svrty_high() { [[ "$COLOR" -ge 2 ]] && tm_out "\033[0;31m$1" || tm_bold "$1"; tm_off; } # litered
+pr_svrty_high() { tm_svrty_high "$1"; [[ "$COLOR" -ge 2 ]] && html_out "<span style=\"color:#cd0000;\">$(html_reserved "$1")</span>" || ( [[ "$COLOR" -eq 1 ]] && html_out "<span style=\"font-weight:bold;\">$(html_reserved "$1")</span>" || html_out "$(html_reserved "$1")" ); }
+tmln_svrty_high() { tm_svrty_high "$1"; tmln_out; }
+prln_svrty_high() { pr_svrty_high "$1"; outln; }
+
+tm_svrty_critical() { [[ "$COLOR" -ge 2 ]] && tm_out "\033[1;31m$1" || tm_bold "$1"; tm_off; } # red
+pr_svrty_critical() { tm_svrty_critical "$1"; [[ "$COLOR" -ge 2 ]] && html_out "<span style=\"color:red;font-weight:bold;\">$(html_reserved "$1")</span>" || ( [[ "$COLOR" -eq 1 ]] && html_out "<span style=\"font-weight:bold;\">$(html_reserved "$1")</span>" || html_out "$(html_reserved "$1")" ); }
+tmln_svrty_critical() { tm_svrty_critical "$1"; tmln_out; }
+prln_svrty_critical() { pr_svrty_critical "$1"; outln; }
+
+tm_deemphasize() { tm_out "$1"; } # hook for a weakened screen output, see #600
+pr_deemphasize() { tm_deemphasize "$1"; html_out "<span style=\"color:darkgray;\">$(html_reserved "$1")</span>"; }
+tmln_deemphasize() { tm_deemphasize "$1"; tmln_out; }
+prln_deemphasize() { pr_deemphasize "$1"; outln; }
+
+# color=1 functions
+tm_off() { [[ "$COLOR" -ne 0 ]] && tm_out "\033[m"; }
+
+tm_bold() { [[ "$COLOR" -ne 0 ]] && tm_out "\033[1m$1" || tm_out "$1"; tm_off; }
+tmln_bold() { tm_bold "$1"; tmln_out; }
+pr_bold() { tm_bold "$1"; [[ "$COLOR" -ne 0 ]] && html_out "<span style=\"font-weight:bold;\">$(html_reserved "$1")</span>" || html_out "$(html_reserved "$1")"; }
+prln_bold() { pr_bold "$1" ; outln; }
+
+NO_ITALICS=false
+if [[ $TERM == screen ]]; then
+ NO_ITALICS=true
+elif [[ $SYSTEM == OpenBSD ]]; then
+ NO_ITALICS=true
+elif [[ $SYSTEM == FreeBSD ]]; then
+ if [[ ${SYSTEMREV%\.*} -le 9 ]]; then
+ NO_ITALICS=true
+ fi
+fi
+tm_italic() { ( [[ "$COLOR" -ne 0 ]] && ! "$NO_ITALICS" ) && tm_out "\033[3m$1" || tm_out "$1"; tm_off; }
+tmln_italic() { tm_italic "$1" ; tmln_out; }
+pr_italic() { tm_italic "$1"; [[ "$COLOR" -ne 0 ]] && html_out "<i>$(html_reserved "$1")</i>" || html_out "$(html_reserved "$1")"; }
+prln_italic() { pr_italic "$1"; outln; }
+
+tm_strikethru() { [[ "$COLOR" -ne 0 ]] && tm_out "\033[9m$1" || tm_out "$1"; tm_off; } # ugly!
+tmln_strikethru() { tm_strikethru "$1"; tmln_out; }
+pr_strikethru() { tm_strikethru "$1"; [[ "$COLOR" -ne 0 ]] && html_out "<strike>$(html_reserved "$1")</strike>" || html_out "$(html_reserved "$1")"; }
+prln_strikethru() { pr_strikethru "$1" ; outln; }
+
+tm_underline() { [[ "$COLOR" -ne 0 ]] && tm_out "\033[4m$1" || tm_out "$1"; tm_off; }
+tmln_underline() { tm_underline "$1"; tmln_out; }
+pr_underline() { tm_underline "$1"; [[ "$COLOR" -ne 0 ]] && html_out "<u>$(html_reserved "$1")</u>" || html_out "$(html_reserved "$1")"; }
+prln_underline() { pr_underline "$1"; outln; }
+
+tm_reverse() { [[ "$COLOR" -ne 0 ]] && tm_out "\033[7m$1" || tm_out "$1"; tm_off; }
+tm_reverse_bold() { [[ "$COLOR" -ne 0 ]] && tm_out "\033[7m\033[1m$1" || tm_out "$1"; tm_off; }
+pr_reverse() { tm_reverse "$1"; [[ "$COLOR" -ne 0 ]] && html_out "<span style=\"color:white;background-color:black;\">$(html_reserved "$1")</span>" || html_out "$(html_reserved "$1")"; }
+pr_reverse_bold() { tm_reverse_bold "$1"; [[ "$COLOR" -ne 0 ]] && html_out "<span style=\"color:white;background-color:black;font-weight:bold;\">$(html_reserved "$1")</span>" || html_out "$(html_reserved "$1")"; }
+
+#pr_headline() { pr_blue "$1"; }
+# https://misc.flogisoft.com/bash/tip_colors_and_formatting
+
+#pr_headline() { [[ "$COLOR" -ge 2 ]] && out "\033[1;30m\033[47m$1" || out "$1"; tm_off; }
+tm_headline() { [[ "$COLOR" -ne 0 ]] && tm_out "\033[1m\033[4m$1" || tm_out "$1"; tm_off; }
+tmln_headline() { tm_headline "$1"; tmln_out; }
+pr_headline() { tm_headline "$1"; [[ "$COLOR" -ne 0 ]] && html_out "<span style=\"text-decoration:underline;font-weight:bold;\">$(html_reserved "$1")</span>" || html_out "$(html_reserved "$1")"; }
+pr_headlineln() { pr_headline "$1" ; outln; }
+
+tm_squoted() { tm_out "'$1'"; }
+pr_squoted() { out "'$1'"; }
+tm_dquoted() { tm_out "\"$1\""; }
+pr_dquoted() { out "\"$1\""; }
+
+# either files couldn't be found or openssl isn't good enough (which shouldn't happen anymore)
+tm_local_problem() { tm_warning "Local problem: $1"; }
+tmln_local_problem() { tmln_warning "Local problem: $1"; }
+pr_local_problem() { pr_warning "Local problem: $1"; }
+prln_local_problem() { prln_warning "Local problem: $1"; }
+
+# general failure
+tm_fixme() { tm_warning "Fixme: $1"; }
+tmln_fixme() { tmln_warning "Fixme: $1"; }
+pr_fixme() { pr_warning "Fixme: $1"; }
+prln_fixme() { prln_warning "Fixme: $1"; }
+
+pr_url() { tm_out "$1"; html_out "<a href=\"$1\" style=\"color:black;text-decoration:none;\">$1</a>"; }
+pr_boldurl() { tm_bold "$1"; html_out "<a href=\"$1\" style=\"font-weight:bold;color:black;text-decoration:none;\">$1</a>"; }
+
+### color switcher (see e.g. https://linuxtidbits.wordpress.com/2008/08/11/output-color-on-bash-scripts/
+### https://www.tldp.org/HOWTO/Bash-Prompt-HOWTO/x405.html
+### no output support for HTML!
+set_color_functions() {
+ local ncurses_tput=true
+
+ if [[ $SYSTEM == OpenBSD ]] && [[ "$TERM" =~ xterm-256 ]]; then
+ export TERM=xterm
+ # OpenBSD can't handle 256 colors (yet) in xterm which might lead to ugly errors
+ # like "tput: not enough arguments (3) for capability `AF'". Not our fault but
+ # before we get blamed we fix it here.
+ fi
+
+ # Empty all vars if we have COLOR=0 equals no escape code -- these are globals:
+ red=""
+ green=""
+ brown=""
+ blue=""
+ magenta=""
+ cyan=""
+ grey=""
+ yellow=""
+ off=""
+ bold=""
+ underline=""
+ italic=""
+
+ type -p tput &>/dev/null || return 0 # Hey wait, do we actually have tput / ncurses ?
+ tput cols &>/dev/null || return 0 # tput under BSDs and GNUs doesn't work either (TERM undefined?)
+ tput sgr0 &>/dev/null || ncurses_tput=false
+ if [[ "$COLOR" -ge 2 ]]; then
+ if $ncurses_tput; then
+ red=$(tput setaf 1)
+ green=$(tput setaf 2)
+ brown=$(tput setaf 3)
+ blue=$(tput setaf 4)
+ magenta=$(tput setaf 5)
+ cyan=$(tput setaf 6)
+ grey=$(tput setaf 7)
+ yellow=$(tput setaf 3; tput bold)
+ else # this is a try for old BSD, see terminfo(5)
+ red=$(tput AF 1)
+ green=$(tput AF 2)
+ brown=$(tput AF 3)
+ blue=$(tput AF 4)
+ magenta=$(tput AF 5)
+ cyan=$(tput AF 6)
+ grey=$(tput AF 7)
+ yellow=$(tput AF 3; tput md)
+ fi
+ fi
+ if [[ "$COLOR" -ge 1 ]]; then
+ if $ncurses_tput; then
+ bold=$(tput bold)
+ underline=$(tput sgr 0 1 2>/dev/null)
+ italic=$(tput sitm) # This doesn't work on FreeBSDi (9,10) and OpenBSD ...
+ italic_end=$(tput ritm) # ... and this, too
+ off=$(tput sgr0)
+ else # this is a try for old BSD, see terminfo(5)
+ bold=$(tput md)
+ underline=$(tput us)
+ italic=$(tput ZH 2>/dev/null) # This doesn't work on FreeBSDi (9,10) and OpenBSD
+ italic_end=$(tput ZR 2>/dev/null) # ... probably entry missing in /etc/termcap
+ reverse=$(tput mr)
+ off=$(tput me)
+ fi
+ fi
+ # FreeBSD 10 understands ESC codes like 'echo -e "\e[3mfoobar\e[23m"', but also no tput for italics
+}
+
+strip_quote() {
+ # remove color codes (see https://www.commandlinefu.com/commands/view/3584/remove-color-codes-special-characters-with-sed)
+ # \', leading and all trailing spaces
+ sed -e "s,$(echo -e "\033")\[[0-9;]*[a-zA-Z],,g" \
+ -e "s/\"/\\'/g" \
+ -e 's/^ *//g' \
+ -e 's/ *$//g' <<< "$1"
+}
+
+# " deconfuse vim\'s syntax highlighting ;-)
+
+#################### JSON FILE FORMATTING ####################
+
+fileout_json_footer() {
+ if "$do_json"; then
+ if [[ "$SCAN_TIME" -eq 0 ]]; then
+ fileout_json_finding "scanTime" "WARN" "Scan interrupted" "" "" ""
+ elif [[ $SEVERITY_LEVEL -lt $LOW ]] ; then
+ # no scan time in --severity=low and above, also needed for Travis, hackish...
+ fileout_json_finding "scanTime" "INFO" $SCAN_TIME "" "" ""
+ fi
+ printf "]\n" >> "$JSONFILE"
+ fi
+ if "$do_pretty_json"; then
+ if [[ "$SCAN_TIME" -eq 0 ]]; then
+ echo -e " ],\n \"scanTime\" : \"Scan interrupted\"\n}" >> "$JSONFILE"
+ else
+ echo -e " ],\n \"scanTime\" : ${SCAN_TIME}\n}" >> "$JSONFILE"
+ fi
+ fi
+}
+
+fileout_json_section() {
+ case $1 in
+ 0) echo -e " \"pretest\" : [" ;;
+ 1) echo -e " \"singleCipher\" : [" ;;
+ 2) echo -e ",\n \"protocols\" : [" ;;
+ 3) echo -e ",\n \"grease\" : [" ;;
+ 4) echo -e ",\n \"ciphers\" : [" ;;
+ 5) echo -e ",\n \"pfs\" : [" ;;
+ 6) echo -e ",\n \"serverPreferences\" : [" ;;
+ 7) echo -e ",\n \"serverDefaults\" : [" ;;
+ 8) echo -e ",\n \"headerResponse\" : [" ;;
+ 9) echo -e ",\n \"vulnerabilities\" : [" ;;
+ 10) echo -e ",\n \"cipherTests\" : [" ;;
+ 11) echo -e ",\n \"browserSimulations\": [" ;;
+ *) echo "invalid section" ;;
+ esac
+}
+
+fileout_section_header() {
+ local str=""
+ "$2" && str="$(fileout_section_footer false)"
+ "$do_pretty_json" && FIRST_FINDING=true && (printf "%s%s\n" "$str" "$(fileout_json_section "$1")") >> "$JSONFILE"
+ SECTION_FOOTER_NEEDED=true
+}
+
+# arg1: whether to end object too
+fileout_section_footer() {
+ "$do_pretty_json" && printf "\n ]" >> "$JSONFILE"
+ "$do_pretty_json" && "$1" && echo -e "\n }" >> "$JSONFILE"
+ SECTION_FOOTER_NEEDED=false
+}
+
+fileout_json_print_parameter() {
+ local parameter="$1"
+ local filler="$2"
+ local value="$3"
+ local not_last="$4"
+ local spaces=""
+
+ "$do_json" && \
+ spaces=" " || \
+ spaces=" "
+ if [[ -n "$value" ]] || [[ "$parameter" == finding ]]; then
+ printf "%s%s%s%s" "$spaces" "\"$parameter\"" "$filler" ": \"$value\"" >> "$JSONFILE"
+ "$not_last" && printf ",\n" >> "$JSONFILE"
+ fi
+}
+
+fileout_json_finding() {
+ local target
+ local finding="$3"
+ local cve="$4"
+ local cwe="$5"
+ local hint="$6"
+
+ if "$do_json"; then
+ "$FIRST_FINDING" || echo -n "," >> "$JSONFILE"
+ echo -e " {" >> "$JSONFILE"
+ fileout_json_print_parameter "id" " " "$1" true
+ fileout_json_print_parameter "ip" " " "$NODE/$NODEIP" true
+ fileout_json_print_parameter "port" " " "$PORT" true
+ fileout_json_print_parameter "severity" " " "$2" true
+ fileout_json_print_parameter "cve" " " "$cve" true
+ fileout_json_print_parameter "cwe" " " "$cwe" true
+ "$GIVE_HINTS" && fileout_json_print_parameter "hint" " " "$hint" true
+ fileout_json_print_parameter "finding" " " "$finding" false
+ echo -e "\n }" >> "$JSONFILE"
+ fi
+ if "$do_pretty_json"; then
+ if [[ "$1" == service ]]; then
+ if [[ $SERVER_COUNTER -gt 1 ]]; then
+ echo " ," >> "$JSONFILE"
+ fi
+ target="$NODE"
+ $do_mx_all_ips && target="$URI"
+ echo -e " {
+ \"targetHost\" : \"$target\",
+ \"ip\" : \"$NODEIP\",
+ \"port\" : \"$PORT\",
+ \"rDNS\" : \"$rDNS\",
+ \"service\" : \"$finding\"," >> "$JSONFILE"
+ $do_mx_all_ips && echo -e " \"hostname\" : \"$NODE\"," >> "$JSONFILE"
+ else
+ ("$FIRST_FINDING" && echo -n " {" >> "$JSONFILE") || echo -n ",{" >> "$JSONFILE"
+ echo -e -n "\n" >> "$JSONFILE"
+ fileout_json_print_parameter "id" " " "$1" true
+ fileout_json_print_parameter "severity" " " "$2" true
+ fileout_json_print_parameter "cve" " " "$cve" true
+ fileout_json_print_parameter "cwe" " " "$cwe" true
+ "$GIVE_HINTS" && fileout_json_print_parameter "hint" " " "$hint" true
+ fileout_json_print_parameter "finding" " " "$finding" false
+ echo -e -n "\n }" >> "$JSONFILE"
+ fi
+ fi
+}
+
+##################### FILE FORMATTING #########################
+
+fileout_pretty_json_banner() {
+ local target
+
+ if ! "$do_mass_testing"; then
+ [[ -z "$NODE" ]] && parse_hn_port "${URI}"
+ # NODE, URL_PATH, PORT, IPADDR and IP46ADDR is set now --> wrong place
+ target="$NODE"
+ $do_mx_all_ips && target="$URI"
+ fi
+
+ echo -e " \"Invocation\" : \"$PROG_NAME $CMDLINE\",
+ \"at\" : \"$HNAME:$OPENSSL_LOCATION\",
+ \"version\" : \"$VERSION ${GIT_REL_SHORT} from $REL_DATE\",
+ \"openssl\" : \"$OSSL_NAME $OSSL_VER from $OSSL_BUILD_DATE\",
+ \"startTime\" : \"$START_TIME\",
+ \"scanResult\" : ["
+}
+
+fileout_banner() {
+ if "$JSONHEADER"; then
+ # "$do_json" && # here we maybe should add a banner, too
+ "$do_pretty_json" && FIRST_FINDING=true && (printf "%s\n" "$(fileout_pretty_json_banner)") >> "$JSONFILE"
+ fi
+}
+
+fileout_separator() {
+ if "$JSONHEADER"; then
+ "$do_pretty_json" && echo " ," >> "$JSONFILE"
+ "$do_json" && echo -n "," >> "$JSONFILE"
+ fi
+}
+
+fileout_footer() {
+ if "$JSONHEADER"; then
+ fileout_json_footer
+ fi
+ # CSV: no footer
+ return 0
+}
+
+fileout_insert_warning() {
+ # See #815. Make sure we don't mess up the JSON PRETTY format if we complain with a client side warning.
+ # This should only be called if an *extra* warning will be printed (previously: 'fileout <extra_warning_ID> "WARN" '
+ # arg1: json identifier, arg2: normally "WARN", arg3: finding
+ #
+ # Also, we have to be careful with any form of mass testing so that a warning won't lead to an invalid JSON
+ # file. As any child will do any check as well (to be reconsidered later), we don't need also the parent to issue
+ # warnings upfront, see #1169. As a detection we'll use --file/-iL as in the children jobs it'll be removed:
+ [[ "$CMDLINE=" =~ --file ]] && return 0
+ [[ "$CMDLINE=" =~ -iL ]] && return 0
+ # Note we still have the message on screen + in HTML which is not as optimal as it could be
+
+ if "$do_pretty_json"; then
+ echo -e " \"clientProblem${CLIENT_PROB_NO}\" : [" >>"$JSONFILE"
+ CLIENT_PROB_NO=$((CLIENT_PROB_NO + 1))
+ FIRST_FINDING=true # make sure we don't have a comma here
+ fi
+ fileout "$1" "$2" "$3"
+ if "$do_pretty_json"; then
+ echo -e "\n ]," >>"$JSONFILE"
+ fi
+}
+
+fileout_csv_finding() {
+ safe_echo "\"$1\"," >> "$CSVFILE"
+ safe_echo "\"$2\"," >> "$CSVFILE"
+ safe_echo "\"$3\"," >> "$CSVFILE"
+ safe_echo "\"$4\"," >> "$CSVFILE"
+ safe_echo "\"$5\"," >> "$CSVFILE"
+ safe_echo "\"$6\"," >> "$CSVFILE"
+ if "$GIVE_HINTS"; then
+ safe_echo "\"$7\"," >> "$CSVFILE"
+ safe_echo "\"$8\"\n" >> "$CSVFILE"
+ else
+ safe_echo "\"$7\"\n" >> "$CSVFILE"
+ fi
+}
+
+
+# ID, SEVERITY, FINDING, CVE, CWE, HINT
+fileout() {
+ local severity="$2"
+ local cve="$4"
+ local cwe="$5"
+ local hint="$6"
+
+ if ( "$do_pretty_json" && [[ "$1" == service ]] ) || show_finding "$severity"; then
+ local finding=$(strip_lf "$(newline_to_spaces "$(strip_quote "$3")")") # additional quotes will mess up screen output
+ [[ -e "$JSONFILE" ]] && [[ ! -d "$JSONFILE" ]] && fileout_json_finding "$1" "$severity" "$finding" "$cve" "$cwe" "$hint"
+ "$do_csv" && [[ -n "$CSVFILE" ]] && [[ ! -d "$CSVFILE" ]] && \
+ fileout_csv_finding "$1" "$NODE/$NODEIP" "$PORT" "$severity" "$finding" "$cve" "$cwe" "$hint"
+ "$FIRST_FINDING" && FIRST_FINDING=false
+ fi
+}
+
+
+json_header() {
+ local fname_prefix
+ local filename_provided=false
+
+ [[ -n "$JSONFILE" ]] && [[ ! -d "$JSONFILE" ]] && filename_provided=true
+ # Similar to HTML: Don't create headers and footers in the following scenarios:
+ # * no JSON/CSV output is being created.
+ # * mass testing is being performed and each test will have its own file.
+ # * this is an individual test within a mass test and all output is being placed in a single file.
+ ! "$do_json" && ! "$do_pretty_json" && JSONHEADER=false && return 0
+ "$do_mass_testing" && ! "$filename_provided" && JSONHEADER=false && return 0
+ "$CHILD_MASS_TESTING" && "$filename_provided" && JSONHEADER=false && return 0
+
+ if "$do_display_only"; then
+ fname_prefix="local-ciphers"
+ elif "$do_mass_testing"; then
+ :
+ elif "$do_mx_all_ips"; then
+ fname_prefix="${FNAME_PREFIX}mx-${URI}"
+ else
+ # ensure NODE, URL_PATH, PORT, IPADDR and IP46ADDR are set
+ ! "$filename_provided" && [[ -z "$NODE" ]] && parse_hn_port "${URI}"
+ fname_prefix="${FNAME_PREFIX}${NODE}_p${PORT}"
+ fi
+ if [[ -z "$JSONFILE" ]]; then
+ JSONFILE="$fname_prefix-$(date +"%Y%m%d-%H%M".json)"
+ elif [[ -d "$JSONFILE" ]]; then
+ JSONFILE="$JSONFILE/${fname_prefix}-$(date +"%Y%m%d-%H%M".json)"
+ fi
+ # Silently reset APPEND var if the file doesn't exist as otherwise it won't be created
+ if "$APPEND" && [[ ! -s "$JSONFILE" ]]; then
+ APPEND=false
+ fi
+ if "$APPEND"; then
+ JSONHEADER=false
+ else
+ [[ -s "$JSONFILE" ]] && fatal "non-empty \"$JSONFILE\" exists. Either use \"--append\" or (re)move it" $ERR_FCREATE
+ "$do_json" && echo "[" > "$JSONFILE"
+ "$do_pretty_json" && echo "{" > "$JSONFILE"
+ fi
+ return 0
+}
+
+
+csv_header() {
+ local fname_prefix
+ local filename_provided=false
+
+ [[ -n "$CSVFILE" ]] && [[ ! -d "$CSVFILE" ]] && filename_provided=true
+ # CSV similar to JSON
+ ! "$do_csv" && CSVHEADER=false && return 0
+ "$do_mass_testing" && ! "$filename_provided" && CSVHEADER=false && return 0
+ "$CHILD_MASS_TESTING" && "$filename_provided" && CSVHEADER=false && return 0
+
+ if "$do_display_only"; then
+ fname_prefix="local-ciphers"
+ elif "$do_mass_testing"; then
+ :
+ elif "$do_mx_all_ips"; then
+ fname_prefix="${FNAME_PREFIX}mx-${URI}"
+ else
+ # ensure NODE, URL_PATH, PORT, IPADDR and IP46ADDR are set
+ ! "$filename_provided" && [[ -z "$NODE" ]] && parse_hn_port "${URI}"
+ fname_prefix="${FNAME_PREFIX}${NODE}_p${PORT}"
+ fi
+ if [[ -z "$CSVFILE" ]]; then
+ CSVFILE="${fname_prefix}-$(date +"%Y%m%d-%H%M".csv)"
+ elif [[ -d "$CSVFILE" ]]; then
+ CSVFILE="$CSVFILE/${fname_prefix}-$(date +"%Y%m%d-%H%M".csv)"
+ fi
+ # Silently reset APPEND var if the file doesn't exist as otherwise it won't be created
+ if "$APPEND" && [[ ! -s "$CSVFILE" ]]; then
+ APPEND=false
+ fi
+ if "$APPEND"; then
+ CSVHEADER=false
+ else
+ [[ -s "$CSVFILE" ]] && fatal "non-empty \"$CSVFILE\" exists. Either use \"--append\" or (re)move it" $ERR_FCREATE
+ touch "$CSVFILE"
+ if "$GIVE_HINTS"; then
+ fileout_csv_finding "id" "fqdn/ip" "port" "severity" "finding" "cve" "cwe" "hint"
+ else
+ fileout_csv_finding "id" "fqdn/ip" "port" "severity" "finding" "cve" "cwe"
+ fi
+ fi
+ return 0
+}
+
+
+################# JSON FILE FORMATTING END. HTML START ####################
+
+html_header() {
+ local fname_prefix
+ local filename_provided=false
+
+ [[ -n "$HTMLFILE" ]] && [[ ! -d "$HTMLFILE" ]] && filename_provided=true
+ # Don't create HTML headers and footers in the following scenarios:
+ # * HTML output is not being created.
+ # * mass testing is being performed and each test will have its own HTML file.
+ # * this is an individual test within a mass test and all HTML output is being placed in a single file.
+ ! "$do_html" && HTMLHEADER=false && return 0
+ "$do_mass_testing" && ! "$filename_provided" && HTMLHEADER=false && return 0
+ "$CHILD_MASS_TESTING" && "$filename_provided" && HTMLHEADER=false && return 0
+
+ if "$do_display_only"; then
+ fname_prefix="local-ciphers"
+ elif "$do_mass_testing"; then
+ :
+ elif "$do_mx_all_ips"; then
+ fname_prefix="${FNAME_PREFIX}mx-${URI}"
+ else
+ # ensure NODE, URL_PATH, PORT, IPADDR and IP46ADDR are set
+ ! "$filename_provided" && [[ -z "$NODE" ]] && parse_hn_port "${URI}"
+ fname_prefix="${FNAME_PREFIX}${NODE}_p${PORT}"
+ fi
+ if [[ -z "$HTMLFILE" ]]; then
+ HTMLFILE="$fname_prefix-$(date +"%Y%m%d-%H%M".html)"
+ elif [[ -d "$HTMLFILE" ]]; then
+ HTMLFILE="$HTMLFILE/$fname_prefix-$(date +"%Y%m%d-%H%M".html)"
+ fi
+ # Silently reset APPEND var if the file doesn't exist as otherwise it won't be created
+ if "$APPEND" && [[ ! -s "$HTMLFILE" ]]; then
+ APPEND=false
+ fi
+ if "$APPEND"; then
+ HTMLHEADER=false
+ else
+ [[ -s "$HTMLFILE" ]] && fatal "non-empty \"$HTMLFILE\" exists. Either use \"--append\" or (re)move it" $ERR_FCREATE
+ html_out "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"
+ html_out "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n"
+ html_out "<!-- This file was created with testssl.sh. https://testssl.sh -->\n"
+ html_out "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n"
+ html_out "<head>\n"
+ html_out "<meta http-equiv=\"Content-Type\" content=\"application/xml+xhtml; charset=UTF-8\" />\n"
+ html_out "<title>testssl.sh</title>\n"
+ html_out "</head>\n"
+ html_out "<body>\n"
+ html_out "<pre>\n"
+ fi
+ return 0
+}
+
+html_banner() {
+ if "$CHILD_MASS_TESTING" && "$HTMLHEADER"; then
+ html_out "## Scan started as: \"$PROG_NAME $CMDLINE\"\n"
+ html_out "## at $HNAME:$OPENSSL_LOCATION\n"
+ html_out "## version testssl: $VERSION ${GIT_REL_SHORT} from $REL_DATE\n"
+ html_out "## version openssl: \"$OSSL_NAME $OSSL_VER\" from \"$OSSL_BUILD_DATE\")\n\n"
+ fi
+}
+
+html_footer() {
+ if "$HTMLHEADER"; then
+ html_out "</pre>\n"
+ html_out "</body>\n"
+ html_out "</html>\n"
+ fi
+ return 0
+}
+
+################# HTML FILE FORMATTING END ####################
+
+prepare_logging() {
+ # arg1: for testing mx records name we put a name of logfile in here, otherwise we get strange file names
+ local fname_prefix="$1"
+ local filename_provided=false
+
+ [[ -n "$LOGFILE" ]] && [[ ! -d "$LOGFILE" ]] && filename_provided=true
+
+ # Similar to html_header():
+ ! "$do_logging" && return 0
+ "$do_mass_testing" && ! "$filename_provided" && return 0
+ "$CHILD_MASS_TESTING" && "$filename_provided" && return 0
+
+ [[ -z "$fname_prefix" ]] && fname_prefix="${FNAME_PREFIX}${NODE}_p${PORT}"
+
+ if [[ -z "$LOGFILE" ]]; then
+ LOGFILE="$fname_prefix-$(date +"%Y%m%d-%H%M".log)"
+ elif [[ -d "$LOGFILE" ]]; then
+ # actually we were instructed to place all files in a DIR instead of the current working dir
+ LOGFILE="$LOGFILE/$fname_prefix-$(date +"%Y%m%d-%H%M".log)"
+ else
+ : # just for clarity: a log file was specified, no need to do anything else
+ fi
+
+ if ! "$APPEND"; then
+ [[ -s "$LOGFILE" ]] && fatal "non-empty \"$LOGFILE\" exists. Either use \"--append\" or (re)move it" $ERR_FCREATE
+ fi
+ tmln_out "## Scan started as: \"$PROG_NAME $CMDLINE\"" >>"$LOGFILE"
+ tmln_out "## at $HNAME:$OPENSSL_LOCATION" >>"$LOGFILE"
+ tmln_out "## version testssl: $VERSION ${GIT_REL_SHORT} from $REL_DATE" >>"$LOGFILE"
+ tmln_out "## version openssl: \"$OSSL_VER\" from \"$OSSL_BUILD_DATE\")\n" >>"$LOGFILE"
+ exec > >(tee -a -i "$LOGFILE")
+}
+
+################### FILE FORMATTING END #########################
+
+###### START helper function definitions ######
+
+if [[ "${BASH_VERSINFO[0]}" == 3 ]]; then
+ # older bash can do this only (MacOS X), even SLES 11, see #697
+ toupper() { tr 'a-z' 'A-Z' <<< "$1"; }
+ tolower() { tr 'A-Z' 'a-z' <<< "$1"; }
+else
+ toupper() { echo -n "${1^^}"; }
+ tolower() { echo -n "${1,,}"; }
+fi
+
+get_last_char() {
+ echo "${1:~0}" # "${string: -1}" would work too (both also in bash 3.2)
+}
+ # Checking for last char. If already a separator supplied, we don't need an additional one
+debugme() {
+ [[ "$DEBUG" -ge 2 ]] && "$@" >&2
+ return 0
+}
+
+hex2dec() {
+ echo $((16#$1))
+}
+
+# convert 414243 into ABC
+hex2ascii() {
+ for (( i=0; i<${#1}; i+=2 )); do
+ # 2>/dev/null added because 'warning: command substitution: ignored null byte in input'
+ # --> didn't help though
+ printf "\x${1:$i:2}" 2>/dev/null
+ done
+}
+
+# convert decimal number < 256 to hex
+dec02hex() {
+ printf "x%02x" "$1"
+}
+
+# convert decimal number between 256 and < 256*256 to hex
+dec04hex() {
+ local a=$(printf "%04x" "$1")
+ printf "x%02s, x%02s" "${a:0:2}" "${a:2:2}"
+}
+
+
+# trim spaces for BSD and old sed
+count_lines() {
+ #echo "${$(wc -l <<< "$1")// /}"
+ # ^^ bad substitution under bash, zsh ok. For some reason this does the trick:
+ echo $(wc -l <<< "$1")
+}
+
+count_words() {
+ #echo "${$(wc -w <<< "$1")// /}"
+ # ^^ bad substitution under bash, zsh ok. For some reason this does the trick:
+ echo $(wc -w <<< "$1")
+}
+
+count_ciphers() {
+ echo $(wc -w <<< "${1//:/ }")
+}
+
+#arg1: TLS 1.2 and below ciphers
+#arg2: TLS 1.3 ciphers
+#arg3: options (e.g., -V)
+actually_supported_osslciphers() {
+ local tls13_ciphers="$TLS13_OSSL_CIPHERS"
+
+ [[ "$2" != ALL ]] && tls13_ciphers="$2"
+ if "$HAS_CIPHERSUITES"; then
+ $OPENSSL ciphers $3 $OSSL_CIPHERS_S -ciphersuites "$tls13_ciphers" "$1" 2>/dev/null || echo ""
+ elif [[ -n "$tls13_ciphers" ]]; then
+ $OPENSSL ciphers $3 $OSSL_CIPHERS_S "$tls13_ciphers:$1" 2>/dev/null || echo ""
+ else
+ $OPENSSL ciphers $OSSL_CIPHERS_S $3 "$1" 2>/dev/null || echo ""
+ fi
+}
+
+# Given a protocol (arg1) and a list of ciphers (arg2) that is formatted as
+# ", xx,xx, xx,xx, xx,xx, xx,xx" remove any TLSv1.3 ciphers if the protocol
+# is less than 04 and remove any TLSv1.2-only ciphers if the protocol is less
+# than 03.
+strip_inconsistent_ciphers() {
+ local -i proto=0x$1
+ local cipherlist="$2"
+
+ [[ $proto -lt 4 ]] && cipherlist="${cipherlist//, 13,0[0-9a-fA-F]/}"
+ if [[ $proto -lt 3 ]]; then
+ cipherlist="${cipherlist//, 00,3[b-fB-F]/}"
+ cipherlist="${cipherlist//, 00,40/}"
+ cipherlist="${cipherlist//, 00,6[7-9a-dA-D]/}"
+ cipherlist="${cipherlist//, 00,9[c-fC-F]/}"
+ cipherlist="${cipherlist//, 00,[abAB][0-9a-fA-F]/}"
+ cipherlist="${cipherlist//, 00,[cC][0-5]/}"
+ cipherlist="${cipherlist//, 16,[bB][7-9aA]/}"
+ cipherlist="${cipherlist//, [cC]0,2[3-9a-fA-F]/}"
+ cipherlist="${cipherlist//, [cC]0,3[01278a-fA-F]/}"
+ cipherlist="${cipherlist//, [cC]0,[4-9aA][0-9a-fA-F]/}"
+ cipherlist="${cipherlist//, [cC][cC],1[345]/}"
+ cipherlist="${cipherlist//, [cC][cC],[aA][89a-eA-E]/}"
+ fi
+ echo "$cipherlist"
+ return 0
+}
+
+newline_to_spaces() {
+ tr '\n' ' ' <<< "$1" | sed 's/ $//'
+}
+
+colon_to_spaces() {
+ echo "${1//:/ }"
+}
+
+strip_lf() {
+ tr -d '\n' <<< "$1" | tr -d '\r'
+}
+
+strip_spaces() {
+ echo "${1// /}"
+}
+
+# https://web.archive.org/web/20121022051228/http://codesnippets.joyent.com/posts/show/1816
+strip_leading_space() {
+ printf "%s" "${1#"${1%%[![:space:]]*}"}"
+}
+strip_trailing_space() {
+ printf "%s" "${1%"${1##*[![:space:]]}"}"
+}
+
+
+# retrieve cipher from ServerHello (via openssl)
+get_cipher() {
+ local cipher=""
+ local server_hello="$(cat -v "$1")"
+ # This and two other following instances are not best practice and normally a useless use of "cat", see
+ # https://web.archive.org/web/20160711205930/http://porkmail.org/era/unix/award.html#uucaletter
+ # However there seem to be cases where the preferred $(< "$1") logic has a problem.
+ # Esepcially with bash 3.2 (Mac OS X) and when on the server side binary chars
+ # are returned, see https://stackoverflow.com/questions/7427262/how-to-read-a-file-into-a-variable-in-shell#22607352
+ # and https://github.com/drwetter/testssl.sh/issues/1292
+ # Performance measurements showed no to barely measureable penalty (1s displayed in 9 tries).
+
+ if [[ "$server_hello" =~ Cipher\ *:\ ([A-Z0-9]+-[A-Za-z0-9\-]+|TLS_[A-Za-z0-9_]+|SSL_[A-Za-z0-9_]+) ]]; then
+ cipher="${BASH_REMATCH##* }"
+ elif [[ "$server_hello" =~ (New|Reused)", "(SSLv[23]|TLSv1(\.[0-3])?(\/SSLv3)?)", Cipher is "([A-Z0-9]+-[A-Za-z0-9\-]+|TLS_[A-Za-z0-9_]+) ]]; then
+ cipher="${BASH_REMATCH##* }"
+ fi
+ tm_out "$cipher"
+}
+
+# retrieve protocol from ServerHello (via openssl)
+get_protocol() {
+ local protocol=""
+ local server_hello="$(cat -v "$1")"
+
+ if [[ "$server_hello" =~ Protocol\ *:\ (SSLv[23]|TLSv1(\.[0-3])?) ]]; then
+ protocol="${BASH_REMATCH##* }"
+ elif [[ "$server_hello" =~ (New|Reused)", TLSv1.3, Cipher is "TLS_[A-Z0-9_]+ ]]; then
+ # Note: When OpenSSL prints "New, <protocol>, Cipher is <cipher>", <cipher> is the
+ # negotiated cipher, but <protocol> is not the negotiated protocol. Instead, it is
+ # the SSL/TLS protocol that first defined <cipher>. Since the ciphers that were
+ # first defined for TLSv1.3 may only be used with TLSv1.3, this line may be used
+ # to determine whether TLSv1.3 was negotiated, but if another protocol is specified
+ # on this line, then this line does not indicate the actual protocol negotiated. Also,
+ # only TLSv1.3 cipher suites have names that begin with TLS_, which provides additional
+ # assurance that the above match will only succeed if TLSv1.3 was negotiated.
+ protocol="TLSv1.3"
+ fi
+ tm_out "$protocol"
+}
+
+is_number() {
+ [[ "$1" =~ ^[1-9][0-9]*$ ]] && \
+ return 0 || \
+ return 1
+}
+
+is_ipv4addr() {
+ local octet="(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])"
+ local ipv4address="$octet\\.$octet\\.$octet\\.$octet"
+
+ [[ -z "$1" ]] && return 1
+ # more than numbers, important for hosts like AAA.BBB.CCC.DDD.in-addr.arpa.DOMAIN.TLS
+ [[ -n $(tr -d '0-9\.' <<< "$1") ]] && return 1
+
+ grep -Eq "$ipv4address" <<< "$1" && \
+ return 0 || \
+ return 1
+}
+
+# a bit easier
+is_ipv6addr() {
+ [[ -z "$1" ]] && return 1
+ # less than 2x ":"
+ [[ $(count_lines "$(tr ':' '\n' <<< "$1")") -le 1 ]] && \
+ return 1
+ #check on chars allowed:
+ [[ -n "$(tr -d '0-9:a-fA-F ' <<< "$1" | sed -e '/^$/d')" ]] && \
+ return 1
+ return 0
+}
+
+# now some function for the integrated BIGIP F5 Cookie detector (see https://github.com/drwetter/F5-BIGIP-Decoder)
+
+f5_hex2ip() {
+ debugme echo "$1"
+ echo $((16#${1:0:2})).$((16#${1:2:2})).$((16#${1:4:2})).$((16#${1:6:2}))
+}
+f5_hex2ip6() {
+ debugme echo "$1"
+ echo "[${1:0:4}:${1:4:4}:${1:8:4}:${1:12:4}.${1:16:4}:${1:20:4}:${1:24:4}:${1:28:4}]"
+}
+
+f5_determine_routeddomain() {
+ local tmp
+ tmp="${1%%o*}"
+ echo "${tmp/rd/}"
+}
+
+f5_ip_oldstyle() {
+ local tmp
+ local a b c d
+
+ tmp="${1/%.*}" # until first dot
+ tmp="$(printf "%08x" "$tmp")" # convert the whole thing to hex, now back to ip (reversed notation:
+ tmp="$(f5_hex2ip $tmp)" # transform to ip with reversed notation
+ IFS="." read -r a b c d <<< "$tmp" # reverse it
+ echo $d.$c.$b.$a
+}
+
+f5_port_decode() {
+ local tmp
+
+ tmp="$(strip_lf "$1")" # remove lf if there is one
+ tmp="${tmp/.0000/}" # to be sure remove trailing zeros with a dot
+ tmp="${tmp#*.}" # get the port
+ tmp="$(printf "%04x" "${tmp}")" # to hex
+ if [[ ${#tmp} -eq 4 ]]; then
+ :
+ elif [[ ${#tmp} -eq 3 ]]; then # fill it up with leading zeros if needed
+ tmp=0${tmp}
+ elif [[ ${#tmp} -eq 2 ]]; then
+ tmp=00${tmp}
+ fi
+ echo $((16#${tmp:2:2}${tmp:0:2})) # reverse order and convert it from hex to dec
+}
+
+
+
+###### END helper function definitions ######
+
+# prints out multiple lines in $1, left aligned by spaces in $2
+out_row_aligned() {
+ local first=true
+
+ while read line; do
+ "$first" && \
+ first=false || \
+ out "$2"
+ outln "$line"
+ done <<< "$1"
+}
+
+# prints text over multiple lines, trying to make no line longer than $max_width.
+# Each line is indented with $spaces.
+out_row_aligned_max_width() {
+ local text="$1"
+ local spaces="$2"
+ local -i max_width="$3"
+ local -i i len
+ local cr=$'\n'
+ local line
+ local first=true
+
+ max_width=$max_width-${#spaces}
+ len=${#text}
+ while true; do
+ if [[ $len -lt $max_width ]]; then
+ # If the remaining text to print is shorter than $max_width,
+ # then just print it.
+ i=$len
+ else
+ # Find the final space character in the text that is less than
+ # $max_width characters into the remaining text, and make the
+ # text up to that space character the next line to print.
+ line="${text:0:max_width}"
+ line="${line% *}"
+ i="${#line}"
+ if [[ $i -eq $max_width ]]; then
+ # If there are no space characters in the first $max_width
+ # characters of the remaining text, then make the text up
+ # to the first space the next line to print. If there are
+ # no space characters in the remaining text, make the
+ # remaining text the next line to print.
+ line="${text#* }"
+ i=$len-${#line}
+ [[ $i -eq 0 ]] && i=$len
+ fi
+ fi
+ if ! "$first"; then
+ tm_out "${cr}${spaces}"
+ fi
+ tm_out "${text:0:i}"
+ [[ $i -eq $len ]] && break
+ len=$len-$i-1
+ i=$i+1
+ text="${text:i:len}"
+ first=false
+ [[ $len -eq 0 ]] && break
+ done
+ return 0
+}
+
+out_row_aligned_max_width_by_entry() {
+ local text="$1"
+ local spaces="$2"
+ local -i max_width="$3"
+ local print_function="$4"
+ local resp entry prev_entry=" "
+
+ resp="$(out_row_aligned_max_width "$text" "$spaces" "$max_width")"
+ while read -d " " entry; do
+ if [[ -n "$entry" ]]; then
+ $print_function "$entry"
+ elif [[ -n "$prev_entry" ]]; then
+ outln; out " "
+ fi
+ out " "
+ prev_entry="$entry"
+ done <<< "$resp"
+}
+
+print_fixed_width() {
+ local text="$1"
+ local -i i len width="$2"
+ local print_function="$3"
+
+ len=${#text}
+ $print_function "$text"
+ for (( i=len; i <= width; i++ )); do
+ out " "
+ done
+}
+
+# saves $TMPFILE or file supplied in $2 under name "$TEMPDIR/$NODEIP.$1".
+# Note: after finishing $TEMPDIR will be removed unless DEBUG >=1
+tmpfile_handle() {
+ local savefile="$2"
+ [[ -z "$savefile" ]] && savefile=$TMPFILE
+#FIXME: make sure/find out if we do not need $TEMPDIR/$NODEIP.$1" if debug=0. We would save fs access here
+ mv $savefile "$TEMPDIR/$NODEIP.$1" 2>/dev/null
+ [[ $ERRFILE =~ dev.null ]] && return 0 || \
+ mv $ERRFILE "$TEMPDIR/$NODEIP.${1//.txt/}.errorlog" 2>/dev/null
+ return 0
+}
+
+# arg1: line with comment sign, tabs and so on
+filter_input() {
+ sed -e 's/#.*$//' -e '/^$/d' <<< "$1" | tr -d '\n' | tr -d '\t' | tr -d '\r'
+}
+
+# Dl's any URL (arg1) via HTTP 1.1 GET from port 80, arg2: file to store http body.
+# Proxy is not honored yet (see cmd line switches) -- except when using curl or wget.
+# There the environment variable is used automatically
+# Currently it is being used by check_revocation_crl() only.
+http_get() {
+ local proto z
+ local node="" query=""
+ local dl="$2"
+ local useragent="$UA_STD"
+ local jsonID="http_get"
+
+ "$SNEAKY" && useragent="$UA_SNEAKY"
+
+ if type -p curl &>/dev/null; then
+ if [[ -z "$PROXY" ]]; then
+ curl -s --noproxy '*' -A $''"$useragent"'' -o $dl "$1"
+ else
+ # for the sake of simplicity assume the proxy is using http
+ curl -s -x $PROXYIP:$PROXYPORT -A $''"$useragent"'' -o $dl "$1"
+ fi
+ return $?
+ elif type -p wget &>/dev/null; then
+ # wget has no proxy command line. We need to use http_proxy instead. And for the sake of simplicity
+ # assume the GET protocol we query is using http -- http_proxy is the $ENV not for the connection TO
+ # the proxy, but for the protocol we query THROUGH the proxy
+ if [[ -z "$PROXY" ]]; then
+ wget --no-proxy -q -U $''"$useragent"'' -O $dl "$1"
+ else
+ if [[ -z "$http_proxy" ]]; then
+ http_proxy=http://$PROXYIP:$PROXYPORT wget -q -U $''"$useragent"'' -O $dl "$1"
+ else
+ wget -q -U $''"$useragent"'' -O $dl "$1"
+ fi
+ fi
+ return $?
+ else
+ # Worst option: slower and hiccups with chunked transfers. Workaround for the
+ # latter is using HTTP/1.0. We do not support https here, yet.
+ # First the URL will be split
+ IFS=/ read -r proto z node query <<< "$1"
+ proto=${proto%:}
+ if [[ "$proto" != http ]]; then
+ pr_warning "protocol $proto not supported yet"
+ fileout "$jsonID" "DEBUG" "protocol $proto not supported yet"
+ return 6
+ fi
+ if [[ -n $PROXY ]]; then
+ # PROXYNODE works better than PROXYIP on modern versions of squid. \
+ # We don't reuse the code in fd_socket() as there's initial CONNECT which makes problems
+ if ! exec 33<> /dev/tcp/${PROXYNODE}/${PROXYPORT}; then
+ outln
+ pr_warning "$PROG_NAME: unable to open a socket to proxy $PROXYNODE:$PROXYPORT"
+ fileout "$jsonID" "DEBUG" "$PROG_NAME: unable to open a socket to proxy $PROXYNODE:$PROXYPORT"
+ return 6
+ else
+ printf -- "%b" "GET $proto://$node/$query HTTP/1.0\r\nUser-Agent: $useragent\r\nHost: $node\r\nAccept: */*\r\n\r\n" >&33
+ fi
+ else
+ IFS=/ read -r proto z node query <<< "$1"
+ exec 33<>/dev/tcp/$node/80
+ printf -- "%b" "GET /$query HTTP/1.0\r\nUser-Agent: $useragent\r\nHost: $node\r\nAccept: */*\r\n\r\n" >&33
+ fi
+ # Strip HTTP header. When in Debug Mode we leave the raw data in place
+ if [[ $DEBUG -ge 1 ]]; then
+ cat <&33 >${dl}.raw
+ cat ${dl}.raw | sed '1,/^[[:space:]]*$/d' >${dl}
+ else
+ cat <&33 | sed '1,/^[[:space:]]*$/d' >${dl}
+ fi
+ exec 33<&-
+ exec 33>&-
+ [[ -s "$dl" ]] && return 0 || return 1
+ fi
+}
+
+# Outputs the headers when downloading any URL (arg1) via HTTP 1.1 GET from port 80.
+# Only works if curl or wget is available.
+# There the environment variable is used automatically
+# Currently it is being used by check_pwnedkeys() only.
+http_get_header() {
+ local proto z
+ local node="" query=""
+ local dl="$2"
+ local useragent="$UA_STD"
+ local jsonID="http_get_header"
+ local headers
+ local -i ret
+
+ "$SNEAKY" && useragent="$UA_SNEAKY"
+
+ if type -p curl &>/dev/null; then
+ if [[ -z "$PROXY" ]]; then
+ headers="$(curl --head -s --noproxy '*' -A $''"$useragent"'' "$1")"
+ else
+ # for the sake of simplicity assume the proxy is using http
+ headers="$(curl --head -s -x $PROXYIP:$PROXYPORT -A $''"$useragent"'' "$1")"
+ fi
+ ret=$?
+ [[ $ret -eq 0 ]] && tm_out "$headers"
+ return $ret
+ elif type -p wget &>/dev/null; then
+ # wget has no proxy command line. We need to use http_proxy instead. And for the sake of simplicity
+ # assume the GET protocol we query is using http -- http_proxy is the $ENV not for the connection TO
+ # the proxy, but for the protocol we query THROUGH the proxy
+ if [[ -z "$PROXY" ]]; then
+ headers="$(wget --no-proxy -q -S -U $''"$useragent"'' -O /dev/null "$1" 2>&1)"
+ else
+ if [[ -z "$http_proxy" ]]; then
+ headers="$(http_proxy=http://$PROXYIP:$PROXYPORT wget -q -S -U $''"$useragent"'' -O /dev/null "$1" 2>&1)"
+ else
+ headers="$(wget -q -S -U $''"$useragent"'' -O /dev/null "$1" 2>&1)"
+ fi
+ fi
+ ret=$?
+ [[ $ret -eq 0 ]] && tm_out "$headers"
+ # wget(1): "8: Server issued an error response.". Happens e.g. when 404 is returned. However also if the call wasn't correct (400)
+ # So we assume for now that everything is submitted correctly. We parse the error code too later
+ [[ $ret -eq 8 ]] && ret=0 && tm_out "$headers"
+ return $ret
+ else
+ return 1
+ fi
+}
+
+ldap_get() {
+ local ldif
+ local -i success
+ local crl="$1"
+ local tmpfile="$2"
+ local jsonID="$3"
+
+ if type -p curl &>/dev/null; then
+ # proxy handling?
+ ldif="$(curl -s "$crl")"
+ [[ $? -eq 0 ]] || return 1
+ awk '/certificateRevocationList/ { print $2 }' <<< "$ldif" | $OPENSSL base64 -d -A -out "$tmpfile" 2>/dev/null
+ [[ -s "$tmpfile" ]] || return 1
+ return 0
+ else
+ pr_litecyan " (for LDAP CRL check install \"curl\")"
+ fileout "$jsonID" "INFO" "LDAP CRL revocation check needs \"curl\""
+ return 2
+ fi
+}
+
+# checks whether the public key in arg1 appears in the https://pwnedkeys.com/ database.
+# arg1: file containing certificate
+# arg2: public key algorithm
+# arg3 key size
+# Responses are as follows:
+# 0 - not checked
+# 1 - key not found in database
+# 2 - key found in database
+# 7 - network/proxy failure
+check_pwnedkeys() {
+ local cert="$1"
+ local cert_key_algo="$2"
+ local -i cert_keysize="$3"
+ local pubkey curve response
+
+ "$PHONE_OUT" || return 0
+
+ # https://pwnedkeys.com only keeps records on 1024 bit and larger RSA keys,
+ # as well as elliptic-curve keys on the P-256, P-384, and P-521 curves.
+ if [[ "$cert_key_algo" =~ RSA ]] || [[ "$cert_key_algo" =~ rsa ]]; then
+ [[ $cert_keysize -ge 1024 ]] || return 0
+ elif [[ "$cert_key_algo" =~ ecdsa ]] || [[ "$cert_key_algo" == *ecPublicKey ]]; then
+ [[ $cert_keysize -eq 256 ]] || [[ $cert_keysize -eq 384 ]] || \
+ [[ $cert_keysize -eq 521 ]] || return 0
+ else
+ return 0
+ fi
+
+ pubkey="$($OPENSSL x509 -in "$cert" -pubkey -noout 2>/dev/null)"
+ # If it is an elliptic curve key, check that it is P-256, P-384, or P-521.
+ if [[ "$cert_key_algo" =~ ecdsa ]] || [[ "$cert_key_algo" == *ecPublicKey ]]; then
+ curve="$($OPENSSL ec -pubin -text <<< "$pubkey" 2>/dev/null)"
+ curve="${curve#*ASN1 OID: }"
+ [[ "$curve" == prime256v1* ]] || [[ "$curve" == secp384r1* ]] || \
+ [[ "$curve" == secp521r1* ]] || return 0
+ fi
+ fingerprint="$($OPENSSL pkey -pubin -outform DER <<< "$pubkey" 2>/dev/null | $OPENSSL dgst -sha256 -hex 2>/dev/null)"
+ fingerprint="${fingerprint#*= }"
+ response="$(http_get_header "https://v1.pwnedkeys.com/$fingerprint")"
+ # Handle curl's/wget's connectivity exit codes
+ case $? in
+ 4|5|7) return 7 ;;
+ 1|2|3|6) return 0 ;;
+ # unknown codes we just say "not checked"
+ esac
+ if [[ "$response" =~ "404 Not Found" ]]; then
+ return 1
+ elif [[ "$response" =~ "200 OK" ]]; then
+ return 2
+ else
+ return 0
+ fi
+}
+
+check_revocation_crl() {
+ local crl="$1"
+ local jsonID="$2"
+ local tmpfile=""
+ local scheme retcode
+ local -i success
+
+ "$PHONE_OUT" || return 0
+ [[ -n "$GOOD_CA_BUNDLE" ]] || return 0
+ scheme="$(tolower "${crl%%://*}")"
+ # The code for obtaining CRLs only supports LDAP, HTTP, and HTTPS URLs.
+ [[ "$scheme" == http ]] || [[ "$scheme" == https ]] || [[ "$scheme" == ldap ]] || return 0
+ tmpfile=$TEMPDIR/${NODE}-${NODEIP}.${crl##*\/} || exit $ERR_FCREATE
+ if [[ "$scheme" == ldap ]]; then
+ ldap_get "$crl" "$tmpfile" "$jsonID"
+ success=$?
+ else
+ http_get "$crl" "$tmpfile"
+ success=$?
+ fi
+ if [[ $success -eq 2 ]]; then
+ return 0
+ elif [[ $success -ne 0 ]]; then
+ out ", "
+ pr_warning "retrieval of \"$crl\" failed"
+ fileout "$jsonID" "WARN" "CRL retrieval from $crl failed"
+ return 1
+ fi
+ # -crl_download could be more elegant but is supported from 1.0.2 onwards only
+ $OPENSSL crl -inform DER -in "$tmpfile" -outform PEM -out "${tmpfile%%.crl}.pem" &>$ERRFILE
+ if [[ $? -ne 0 ]]; then
+ pr_warning "conversion of \"$tmpfile\" failed"
+ fileout "$jsonID" "WARN" "conversion of CRL to PEM format failed"
+ return 1
+ fi
+ if grep -q "\-\-\-\-\-BEGIN CERTIFICATE\-\-\-\-\-" $TEMPDIR/intermediatecerts.pem; then
+ $OPENSSL verify -crl_check -CAfile <(cat $ADDITIONAL_CA_FILES "$GOOD_CA_BUNDLE" "${tmpfile%%.crl}.pem") -untrusted $TEMPDIR/intermediatecerts.pem $HOSTCERT &> "${tmpfile%%.crl}.err"
+ else
+ $OPENSSL verify -crl_check -CAfile <(cat $ADDITIONAL_CA_FILES "$GOOD_CA_BUNDLE" "${tmpfile%%.crl}.pem") $HOSTCERT &> "${tmpfile%%.crl}.err"
+ fi
+ if [[ $? -eq 0 ]]; then
+ out ", "
+ pr_svrty_good "not revoked"
+ fileout "$jsonID" "OK" "not revoked"
+ else
+ retcode=$(awk '/error [1-9][0-9]? at [0-9]+ depth lookup:/ { if (!found) {print $2; found=1} }' "${tmpfile%%.crl}.err")
+ if [[ "$retcode" == 23 ]]; then # see verify_retcode_helper()
+ out ", "
+ pr_svrty_critical "revoked"
+ fileout "$jsonID" "CRITICAL" "revoked"
+ else
+ retcode="$(verify_retcode_helper "$retcode")"
+ out " $retcode"
+ retcode="${retcode#(}"
+ retcode="${retcode%)}"
+ fileout "$jsonID" "WARN" "$retcode"
+ if [[ $DEBUG -ge 2 ]]; then
+ outln
+ cat "${tmpfile%%.crl}.err"
+ fi
+ fi
+ fi
+ return 0
+}
+
+check_revocation_ocsp() {
+ local uri="$1"
+ local stapled_response="$2"
+ local jsonID="$3"
+ local tmpfile=""
+ local -i success
+ local response=""
+ local host_header=""
+
+ "$PHONE_OUT" || [[ -n "$stapled_response" ]] || return 0
+ [[ -n "$GOOD_CA_BUNDLE" ]] || return 0
+ if [[ -n "$PROXY" ]] && ! "$IGN_OCSP_PROXY"; then
+ # see #1106 and https://github.com/openssl/openssl/issues/6965
+ out ", "
+ pr_warning "revocation not tested as \"openssl ocsp\" doesn't support a proxy"
+ fileout "$jsonID" "WARN" "Revocation not tested as openssl ocsp doesn't support a proxy"
+ return 0
+ fi
+ grep -q "\-\-\-\-\-BEGIN CERTIFICATE\-\-\-\-\-" $TEMPDIR/intermediatecerts.pem || return 0
+ tmpfile=$TEMPDIR/${NODE}-${NODEIP}.${uri##*\/} || exit $ERR_FCREATE
+ if [[ -n "$stapled_response" ]]; then
+ asciihex_to_binary "$stapled_response" > "$TEMPDIR/stapled_ocsp_response.dd"
+ $OPENSSL ocsp -no_nonce -respin "$TEMPDIR/stapled_ocsp_response.dd" \
+ -issuer $TEMPDIR/hostcert_issuer.pem -verify_other $TEMPDIR/intermediatecerts.pem \
+ -CAfile <(cat $ADDITIONAL_CA_FILES "$GOOD_CA_BUNDLE") -cert $HOSTCERT -text &> "$tmpfile"
+ else
+ host_header=${uri##http://}
+ host_header=${host_header%%/*}
+ if [[ "$OSSL_NAME" =~ LibreSSL ]]; then
+ host_header="-header Host ${host_header}"
+ elif [[ $OSSL_VER_MAJOR.$OSSL_VER_MINOR == 1.1.0* ]] || [[ $OSSL_VER_MAJOR.$OSSL_VER_MINOR == 1.1.1* ]] || \
+ [[ $OSSL_VER_MAJOR.$OSSL_VER_MINOR == 3.0.0* ]]; then
+ host_header="-header Host=${host_header}"
+ else
+ host_header="-header Host ${host_header}"
+ fi
+ $OPENSSL ocsp -no_nonce ${host_header} -url "$uri" \
+ -issuer $TEMPDIR/hostcert_issuer.pem -verify_other $TEMPDIR/intermediatecerts.pem \
+ -CAfile <(cat $ADDITIONAL_CA_FILES "$GOOD_CA_BUNDLE") -cert $HOSTCERT -text &> "$tmpfile"
+ fi
+ if [[ $? -eq 0 ]] && grep -Fq "Response verify OK" "$tmpfile"; then
+ response="$(grep -F "$HOSTCERT: " "$tmpfile")"
+ response="${response#$HOSTCERT: }"
+ response="${response%\.}"
+ if [[ "$response" =~ "good" ]]; then
+ out ", "
+ pr_svrty_good "not revoked"
+ fileout "$jsonID" "OK" "not revoked"
+ elif [[ "$response" =~ "revoked" ]]; then
+ out ", "
+ pr_svrty_critical "revoked"
+ fileout "$jsonID" "CRITICAL" "revoked"
+ else
+ out ", "
+ pr_warning "error querying OCSP responder"
+ fileout "$jsonID" "WARN" "$response"
+ if [[ $DEBUG -ge 2 ]]; then
+ outln
+ cat "$tmpfile"
+ else
+ out " ($response)"
+ fi
+ fi
+ else
+ [[ -s "$tmpfile" ]] || response="empty ocsp response"
+ [[ -z "$response" ]] && response="$(awk '/Responder Error:/ { print $3 }' "$tmpfile")"
+ [[ -z "$response" ]] && grep -Fq "Response Verify Failure" "$tmpfile" && response="unable to verify response"
+ [[ -z "$response" ]] && response="$(awk -F':' '/Code/ { print $NF }' $tmpfile)"
+ out ", "
+ pr_warning "error querying OCSP responder"
+ fileout "$jsonID" "WARN" "$response"
+ if [[ $DEBUG -ge 2 ]]; then
+ outln
+ [[ -s "$tmpfile" ]] && cat "$tmpfile" || echo "empty ocsp response"
+ elif [[ -n "$response" ]]; then
+ out " ($response)"
+ fi
+ fi
+}
+
+wait_kill(){
+ local pid=$1 # pid we wait for or kill
+ local maxsleep=$2 # how long we wait before killing
+
+ HAD_SLEPT=0
+ while true; do
+ if ! ps $pid >/dev/null ; then
+ return 0 # process terminated before didn't reach $maxsleep
+ fi
+ [[ "$DEBUG" -ge 6 ]] && ps $pid
+ sleep 1
+ maxsleep=$((maxsleep - 1))
+ HAD_SLEPT=$((HAD_SLEPT + 1))
+ test $maxsleep -le 0 && break
+ done # needs to be killed:
+ kill $pid >&2 2>/dev/null
+ wait $pid 2>/dev/null # make sure pid terminated, see wait(1p)
+ return 3 # means killed
+}
+
+# Convert date formats -- we always use GMT=UTC here
+# argv1: source date string
+# argv2: dest date string
+if "$HAS_GNUDATE"; then # Linux and NetBSD
+ parse_date() {
+ LC_ALL=C TZ=GMT date -d "$1" "$2"
+ }
+elif "$HAS_FREEBSDDATE"; then # FreeBSD, OS X and newer (~6.6) OpenBSD versions
+ parse_date() {
+ LC_ALL=C TZ=GMT date -j -f "$3" "$2" "$1"
+ }
+elif "$HAS_OPENBSDDATE"; then
+# We basically echo it as a conversion as we want it is too difficult. Approach for that would be:
+# printf '%s\n' "$1" | awk '{ printf "%04d%02d%02d\n", $4, $2, (index("JanFebMarAprMayJunJulAugSepOctNovDec",$1)+2)/3}'
+# 4: year, 1: month, 2: day, $3: time (e.g. "Dec 8 10:16:13 2016")
+# This way we could also kind of convert args to epoch but as newer OpenBSDs "date" behave like FreeBSD
+ parse_date() {
+ local tmp=""
+ if [[ $2 == +%s* ]]; then
+ echo "${1// GMT}"
+ else
+ tmp="$(printf '%s\n' "$1" | awk '{ printf "%04d-%02d-%02d %08s\n", $4, (index("JanFebMarAprMayJunJulAugSepOctNovDec",$1)+2)/3, $2, $3 }')"
+ echo "${tmp%:*}" # remove seconds, result now is in line with GNU date 2016-12-08 10:16
+ fi
+ }
+else
+ parse_date() {
+ LC_ALL=C TZ=GMT date -j "$2" "$1"
+ }
+fi
+
+# arg1: An ASCII-HEX string
+# Print $arg1 in binary format
+asciihex_to_binary() {
+ local string="$1"
+ local -i len
+ local -i i ip2 ip4 ip6 ip8 ip10 ip12 ip14
+ local -i remainder
+
+ len=${#string}
+ [[ $len%2 -ne 0 ]] && return 1
+
+ for (( i=0; i <= len-16 ; i=i+16 )); do
+ ip2=$((i+2)); ip4=$((i+4)); ip6=$((i+6)); ip8=$((i+8)); ip10=$((i+10)); ip12=$((i+12)); ip14=$((i+14))
+ printf -- "\x${string:i:2}\x${string:ip2:2}\x${string:ip4:2}\x${string:ip6:2}\x${string:ip8:2}\x${string:ip10:2}\x${string:ip12:2}\x${string:ip14:2}"
+ done
+
+ ip2=$((i+2)); ip4=$((i+4)); ip6=$((i+6)); ip8=$((i+8)); ip10=$((i+10)); ip12=$((i+12)); ip14=$((i+14))
+ remainder=$len-$i
+ case $remainder in
+ 2) printf -- "\x${string:i:2}" ;;
+ 4) printf -- "\x${string:i:2}\x${string:ip2:2}" ;;
+ 6) printf -- "\x${string:i:2}\x${string:ip2:2}\x${string:ip4:2}" ;;
+ 8) printf -- "\x${string:i:2}\x${string:ip2:2}\x${string:ip4:2}\x${string:ip6:2}" ;;
+ 10) printf -- "\x${string:i:2}\x${string:ip2:2}\x${string:ip4:2}\x${string:ip6:2}\x${string:ip8:2}" ;;
+ 12) printf -- "\x${string:i:2}\x${string:ip2:2}\x${string:ip4:2}\x${string:ip6:2}\x${string:ip8:2}\x${string:ip10:2}" ;;
+ 14) printf -- "\x${string:i:2}\x${string:ip2:2}\x${string:ip4:2}\x${string:ip6:2}\x${string:ip8:2}\x${string:ip10:2}\x${string:ip12:2}" ;;
+ esac
+ return 0
+}
+
+# arg1: text string
+# Output a comma-separated ASCII-HEX string representation of the input string.
+string_to_asciihex() {
+ local string="$1"
+ local -i i eos
+ local output=""
+
+ eos=${#string}-1
+ for (( i=0; i<eos; i++ )); do
+ output+="$(printf "%02x," "'${string:i:1}")"
+ done
+ [[ -n "$string" ]] && output+="$(printf "%02x" "'${string:eos:1}")"
+ tm_out "$output"
+ return 0
+
+}
+
+# Adjust options to $OPENSSL s_client based on OpenSSL version and protocol version
+s_client_options() {
+ local options=" $1"
+ local ciphers="notpresent" tls13_ciphers="notpresent"
+
+ # Extract the TLSv1.3 ciphers and the non-TLSv1.3 ciphers
+ if [[ " $options " =~ " -cipher " ]]; then
+ ciphers="${options#* -cipher }"
+ ciphers="${ciphers%% *}"
+ options="${options//-cipher $ciphers/}"
+ ciphers="${ciphers##\'}"
+ ciphers="${ciphers%%\'}"
+ fi
+ if [[ " $options " =~ " -ciphersuites " ]]; then
+ tls13_ciphers="${options#* -ciphersuites }"
+ tls13_ciphers="${tls13_ciphers%% *}"
+ options="${options//-ciphersuites $tls13_ciphers/}"
+ tls13_ciphers="${tls13_ciphers##\'}"
+ tls13_ciphers="${tls13_ciphers%%\'}"
+ [[ "$tls13_ciphers" == ALL ]] && tls13_ciphers="$TLS13_OSSL_CIPHERS"
+ fi
+
+ # Don't include the -servername option for an SSLv2 or SSLv3 ClientHello.
+ [[ -n "$SNI" ]] && [[ " $options " =~ \ -ssl[2|3]\ ]] && options="${options//$SNI/}"
+
+ # The server_name extension should not be included in the ClientHello unless
+ # the -servername option is provided. However, OpenSSL 1.1.1 will include the
+ # server_name extension unless the -noservername option is provided. So, if
+ # the command line doesn't include -servername and the -noservername option is
+ # supported, then add -noservername to the options.
+ "$HAS_NOSERVERNAME" && [[ ! " $options " =~ " -servername " ]] && options+=" -noservername"
+
+ # Newer versions of OpenSSL have dropped support for the -no_ssl2 option, so
+ # remove any -no_ssl2 option if the option isn't supported. (Since versions of
+ # OpenSSL that don't support -no_ssl2 also don't support SSLv2, the option
+ # isn't needed for these versions of OpenSSL.)
+ ! "$HAS_NO_SSL2" && options="${options//-no_ssl2/}"
+
+ # At least one server will fail under some circumstances if compression methods are offered.
+ # So, only offer compression methods if necessary for the test. In OpenSSL 1.1.0 and
+ # 1.1.1 compression is only offered if the "-comp" option is provided.
+ # OpenSSL 1.0.0, 1.0.1, and 1.0.2 offer compression unless the "-no_comp" option is provided.
+ # OpenSSL 0.9.8 does not support either the "-comp" or the "-no_comp" option.
+ if [[ " $options " =~ " -comp " ]]; then
+ # Compression is needed for the test. So, remove "-comp" if it isn't supported, but
+ # otherwise make no changes.
+ ! "$HAS_COMP" && options="${options//-comp/}"
+ else
+ # Compression is not needed. So, specify "-no_comp" if that option is supported.
+ "$HAS_NO_COMP" && options+=" -no_comp"
+ fi
+
+ # If $OPENSSL is compiled with TLSv1.3 support and s_client is called without
+ # specifying a protocol, but specifying a list of ciphers that doesn't include
+ # any TLSv1.3 ciphers, then the command will always fail. So, if $OPENSSL supports
+ # TLSv1.3 and a cipher list is provided, but no protocol is specified, then add
+ # -no_tls1_3 if no TLSv1.3 ciphers are provided.
+ if "$HAS_TLS13" && [[ "$ciphers" != notpresent ]] && \
+ ( [[ "$tls13_ciphers" == notpresent ]] || [[ -z "$tls13_ciphers" ]] ) && \
+ [[ ! " $options " =~ \ -ssl[2|3]\ ]] && \
+ [[ ! " $options " =~ \ -tls1\ ]] && \
+ [[ ! " $options " =~ \ -tls1_[1|2|3]\ ]]; then
+ options+=" -no_tls1_3"
+ fi
+
+ if [[ "$ciphers" != notpresent ]] || [[ "$tls13_ciphers" != notpresent ]]; then
+ if ! "$HAS_CIPHERSUITES"; then
+ [[ "$ciphers" == notpresent ]] && ciphers=""
+ [[ "$tls13_ciphers" == notpresent ]] && tls13_ciphers=""
+ [[ -n "$ciphers" ]] && [[ -n "$tls13_ciphers" ]] && ciphers=":$ciphers"
+ ciphers="$tls13_ciphers$ciphers"
+ options+=" -cipher $ciphers"
+ else
+ if [[ "$ciphers" != notpresent ]] && [[ -n "$ciphers" ]]; then
+ options+=" -cipher $ciphers"
+ fi
+ if [[ "$tls13_ciphers" != notpresent ]] && [[ -n "$tls13_ciphers" ]]; then
+ options+=" -ciphersuites $tls13_ciphers"
+ fi
+ fi
+ fi
+
+ # OpenSSL's name for secp256r1 is prime256v1. So whenever we encounter this
+ # (e.g. client simulations) we replace it with the name which OpenSSL understands
+ # This shouldn't be needed. We have this here as a last resort
+ if [[ "$1" =~ " -curves " ]]; then
+ ! "$HAS_CURVES" && options="${options// -curves / -groups }"
+ [[ "$1" =~ secp192r1 ]] && options="${options//secp192r1/prime192v1}"
+ [[ "$1" =~ secp256r1 ]] && options="${options//secp256r1/prime256v1}"
+ fi
+ tm_out "$options $keyopts"
+}
+
+###### check code starts here ######
+
+# determines whether the port has an HTTP service running or not (plain TLS, no STARTTLS)
+# arg1 could be the protocol determined as "working". IIS6 needs that
+service_detection() {
+ local -i was_killed
+
+ if ! "$CLIENT_AUTH"; then
+ # SNI is not standardized for !HTTPS but fortunately for other protocols s_client doesn't seem to care
+ printf "$GET_REQ11" | $OPENSSL s_client $(s_client_options "$1 -quiet $BUGS -connect $NODEIP:$PORT $PROXY $SNI") >$TMPFILE 2>$ERRFILE &
+ wait_kill $! $HEADER_MAXSLEEP
+ was_killed=$?
+ head $TMPFILE | grep -aq '^HTTP\/' && SERVICE=HTTP
+ [[ -z "$SERVICE" ]] && head $TMPFILE | grep -waq "SMTP|ESMTP|Exim|IdeaSmtpServer|Kerio Connect|Postfix" && SERVICE=SMTP # I know some overlap here
+ [[ -z "$SERVICE" ]] && head $TMPFILE | grep -Ewaq "POP|Gpop|MailEnable POP3 Server|OK Dovecot|Cyrus POP3" && SERVICE=POP # I know some overlap here
+ [[ -z "$SERVICE" ]] && head $TMPFILE | grep -Ewaq "IMAP|IMAP4|Cyrus IMAP4IMAP4rev1|IMAP4REV1|Gimap" && SERVICE=IMAP # I know some overlap here
+ [[ -z "$SERVICE" ]] && head $TMPFILE | grep -aq FTP && SERVICE=FTP
+ [[ -z "$SERVICE" ]] && head $TMPFILE | grep -Eaqi "jabber|xmpp" && SERVICE=XMPP
+ [[ -z "$SERVICE" ]] && head $TMPFILE | grep -Eaqw "Jive News|InterNetNews|NNRP|INN|Kerio Connect|NNTP Service|Kerio MailServer|NNTP server" && SERVICE=NNTP
+ # MongoDB port 27017 will respond to a GET request with a mocked HTTP response
+ [[ "$SERVICE" == HTTP ]] && head $TMPFILE | grep -Eaqw "MongoDB" && SERVICE=MongoDB
+ debugme head -50 $TMPFILE | sed -e '/<HTML>/,$d' -e '/<html>/,$d' -e '/<XML/,$d' -e '/<xml/,$d' -e '/<\?XML/,$d' -e '/<\?xml/,$d' -e '/<\!DOCTYPE/,$d' -e '/<\!doctype/,$d'
+ fi
+
+ out " Service detected: $CORRECT_SPACES"
+ jsonID="service"
+ case $SERVICE in
+ HTTP)
+ out " $SERVICE"
+ fileout "${jsonID}" "INFO" "$SERVICE"
+ ;;
+ IMAP|POP|SMTP|NNTP|MongoDB)
+ out " $SERVICE, thus skipping HTTP specific checks"
+ fileout "${jsonID}" "INFO" "$SERVICE, thus skipping HTTP specific checks"
+ ;;
+ *) if "$CLIENT_AUTH"; then
+ out " certificate-based authentication => skipping all HTTP checks"
+ echo "certificate-based authentication => skipping all HTTP checks" >$TMPFILE
+ fileout "${jsonID}" "INFO" "certificate-based authentication => skipping all HTTP checks"
+ else
+ out " Couldn't determine what's running on port $PORT"
+ if "$ASSUME_HTTP"; then
+ SERVICE=HTTP
+ out " -- ASSUME_HTTP set though"
+ fileout "${jsonID}" "DEBUG" "Couldn't determine service -- ASSUME_HTTP set"
+ else
+ out ", assuming no HTTP service => skipping all HTTP checks"
+ fileout "${jsonID}" "DEBUG" "Couldn't determine service, skipping all HTTP checks"
+ fi
+ fi
+ ;;
+ esac
+
+ outln "\n"
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 0
+}
+
+# 1: counter variable
+# 2: threshold for this variable
+# 3: string for first occurrence of problem
+# 4: string for repeated occurrence of problem
+#
+connectivity_problem() {
+ if [[ $1 -lt $2 ]]; then
+ if "$TLS13_ONLY" && ! "$HAS_TLS13"; then
+ :
+ else
+ prln_warning " Oops: $3"
+ fi
+ return 0
+ fi
+ if [[ $1 -ge $2 ]]; then
+ if [[ "$4" =~ openssl\ s_client\ connect ]] ; then
+ fatal "$4" $ERR_CONNECT "Consider increasing MAX_OSSL_FAIL (currently: $2)"
+ elif [[ "$4" =~ repeated\ TCP\ connect ]]; then
+ fatal "$4" $ERR_CONNECT "Consider increasing MAX_SOCKET_FAIL (currently: $2)"
+ fi
+ fatal "$4" $ERR_CONNECT
+ fi
+}
+
+
+#problems not handled: chunked
+run_http_header() {
+ local header
+ local referer useragent
+ local url redirect
+ local jsonID="HTTP_status_code"
+ local spaces=" "
+
+ HEADERFILE=$TEMPDIR/$NODEIP.http_header.txt
+ if [[ $NR_HEADER_FAIL -eq 0 ]]; then
+ # skip repeating this line if it's 2nd, 3rd,.. try
+ outln; pr_headlineln " Testing HTTP header response @ \"$URL_PATH\" "
+ outln
+ fi
+ if [[ $NR_HEADER_FAIL -ge $MAX_HEADER_FAIL ]]; then
+ # signal to caller we have a problem
+ return 1
+ fi
+
+ pr_bold " HTTP Status Code "
+ [[ -z "$1" ]] && url="/" || url="$1"
+ printf "$GET_REQ11" | $OPENSSL s_client $(s_client_options "$OPTIMAL_PROTO $BUGS -quiet -ign_eof -connect $NODEIP:$PORT $PROXY $SNI") >$HEADERFILE 2>$ERRFILE &
+ wait_kill $! $HEADER_MAXSLEEP
+ if [[ $? -eq 0 ]]; then
+ # Issue HTTP GET again as it properly finished within $HEADER_MAXSLEEP and didn't hang.
+ # Doing it again in the foreground to get an accurate header time
+ printf "$GET_REQ11" | $OPENSSL s_client $(s_client_options "$OPTIMAL_PROTO $BUGS -quiet -ign_eof -connect $NODEIP:$PORT $PROXY $SNI") >$HEADERFILE 2>$ERRFILE
+ NOW_TIME=$(date "+%s")
+ HTTP_TIME=$(awk -F': ' '/^date:/ { print $2 } /^Date:/ { print $2 }' $HEADERFILE)
+ HAD_SLEPT=0
+ else
+ # 1st GET request hung and needed to be killed. Check whether it succeeded anyway:
+ if grep -Eiaq "XML|HTML|DOCTYPE|HTTP|Connection" $HEADERFILE; then
+ # correct by seconds we slept, HAD_SLEPT comes from wait_kill()
+ NOW_TIME=$(($(date "+%s") - HAD_SLEPT))
+ HTTP_TIME=$(awk -F': ' '/^date:/ { print $2 } /^Date:/ { print $2 }' $HEADERFILE)
+ else
+ prln_warning " likely HTTP header requests failed (#lines: $(wc -l $HEADERFILE | awk '{ print $1 }'))"
+ [[ "$DEBUG" -lt 1 ]] && outln "Rerun with DEBUG>=1 and inspect $HEADERFILE\n"
+ fileout "HTTP_status_code" "WARN" "HTTP header request failed"
+ debugme cat $HEADERFILE
+ ((NR_HEADER_FAIL++))
+ fi
+ fi
+ if [[ ! -s $HEADERFILE ]]; then
+ ((NR_HEADER_FAIL++))
+ if [[ $NR_HEADER_FAIL -ge $MAX_HEADER_FAIL ]]; then
+ # Now, try to give a hint whether it would make sense to try with OpenSSL 1.1.0 or 1.1.1 instead
+ if [[ $CURVES_OFFERED == X448 ]] && ! "$HAS_X448" ; then
+ generic_nonfatal "HTTP header was repeatedly zero due to missing X448 curve." "${spaces}OpenSSL 1.1.1 might help. Skipping complete HTTP header section."
+ elif [[ $CURVES_OFFERED == X25519 ]] && ! "$HAS_X25519" ; then
+ generic_nonfatal "HTTP header was repeatedly zero due to missing X25519 curve." "${spaces}OpenSSL 1.1.0 might help. Skipping complete HTTP header section."
+ elif [[ $CURVES_OFFERED =~ X25519 ]] && [[ $CURVES_OFFERED =~ X448 ]] && ! "$HAS_X25519" && ! "$HAS_X448"; then
+ generic_nonfatal "HTTP header was repeatedly zero due to missing X25519/X448 curves." "${spaces}OpenSSL >=1.1.0 might help. Skipping complete HTTP header section."
+ else
+ # we could give more hints but these are the most likely cases
+ generic_nonfatal "HTTP header was repeatedly zero." "Skipping complete HTTP header section."
+ fi
+ KNOWN_OSSL_PROB=true
+ return 1
+ else
+ pr_warning "HTTP header reply empty. "
+ fileout "$jsonID" "WARN" "HTTP header reply empty"
+ fi
+ fi
+
+ # Populate vars for HTTP time
+ debugme echo "NOW_TIME: $NOW_TIME | HTTP_TIME: $HTTP_TIME"
+
+ # Quit on first empty line to catch 98% of the cases. Next pattern is there because the SEDs tested
+ # so far seem not to be fine with header containing x0d x0a (CRLF) which is the usual case.
+ # So we also trigger also on any sign on a single line which is not alphanumeric (plus _)
+ sed -e '/^$/q' -e '/^[^a-zA-Z_0-9]$/q' $HEADERFILE >$HEADERFILE.tmp
+ # Now to be more sure we delete from '<' or '{' maybe with a leading blank until the end
+ sed -e '/^ *<.*$/d' -e '/^ *{.*$/d' $HEADERFILE.tmp >$HEADERFILE
+ debugme echo -e "---\n $(< $HEADERFILE) \n---"
+
+ HTTP_STATUS_CODE=$(awk '/^HTTP\// { print $2 }' $HEADERFILE 2>>$ERRFILE)
+ msg_thereafter=$(awk -F"$HTTP_STATUS_CODE" '/^HTTP\// { print $2 }' $HEADERFILE 2>>$ERRFILE) # dirty trick to use the status code as a
+ msg_thereafter=$(strip_lf "$msg_thereafter") # field separator, otherwise we need a loop with awk
+ debugme echo "Status/MSG: $HTTP_STATUS_CODE $msg_thereafter"
+
+ [[ -n "$HTTP_STATUS_CODE" ]] && out " $HTTP_STATUS_CODE$msg_thereafter"
+ case $HTTP_STATUS_CODE in
+ 301|302|307|308)
+ redirect=$(grep -a '^Location' $HEADERFILE | sed 's/Location: //' | tr -d '\r\n')
+ out ", redirecting to \""; pr_url "$redirect"; out "\""
+ if [[ $redirect =~ http:// ]]; then
+ pr_svrty_high " -- Redirect to insecure URL (NOT ok)"
+ fileout "insecure_redirect" "HIGH" "Redirect to insecure URL: \"$redirect\""
+ fi
+ fileout "$jsonID" "INFO" "$HTTP_STATUS_CODE$msg_thereafter (\"$URL_PATH\")"
+ ;;
+ 200|204|403|405)
+ fileout "$jsonID" "INFO" "$HTTP_STATUS_CODE$msg_thereafter (\"$URL_PATH\")"
+ ;;
+ 206)
+ out " -- WHAT?"
+ fileout "$jsonID" "INFO" "$HTTP_STATUS_CODE$msg_thereafter (\"$URL_PATH\") -- WHAT?"
+ # partial content shouldn't happen
+ ;;
+ 400)
+ pr_cyan " (Hint: better try another URL)"
+ fileout "$jsonID" "INFO" "$HTTP_STATUS_CODE$msg_thereafter (\"$URL_PATH\") -- better try another URL"
+ ;;
+ 401)
+ grep -aq "^WWW-Authenticate" $HEADERFILE && out " "; out "$(strip_lf "$(grep -a "^WWW-Authenticate" $HEADERFILE)")"
+ fileout "$jsonID" "INFO" "$HTTP_STATUS_CODE$msg_thereafter (\"$URL_PATH\") -- $(grep -a "^WWW-Authenticate" $HEADERFILE)"
+ ;;
+ 404)
+ out " (Hint: supply a path which doesn't give a \"$HTTP_STATUS_CODE$msg_thereafter\")"
+ fileout "$jsonID" "INFO" "$HTTP_STATUS_CODE$msg_thereafter (\"$URL_PATH\")"
+ ;;
+ "")
+ prln_warning "No HTTP status code."
+ fileout "$jsonID" "WARN" "No HTTP status code"
+ return 1
+ ;;
+ *)
+ pr_warning ". Oh, didn't expect \"$HTTP_STATUS_CODE$msg_thereafter\""
+ fileout "$jsonID" "WARN" "Unexpected $HTTP_STATUS_CODE$msg_thereafter @ \"$URL_PATH\""
+ ;;
+ esac
+ outln
+
+ # we don't call "tmpfile_handle ${FUNCNAME[0]}.txt" as we need the header file in other functions!
+ return 0
+}
+
+# Borrowed from Glenn Jackman, see https://unix.stackexchange.com/users/4667/glenn-jackman
+#
+match_ipv4_httpheader() {
+ local octet="(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])"
+ local ipv4address="$octet\\.$octet\\.$octet\\.$octet"
+ local whitelisted_header="pagespeed|page-speed|^Content-Security-Policy|^MicrosoftSharePointTeamServices|^X-OWA-Version|^Location|^Server: PRTG"
+ local your_ip_msg="(check if it's your IP address or e.g. a cluster IP)"
+ local result
+ local first=true
+ local spaces=" "
+ local count
+ local jsonID="ipv4_in_header"
+ local cwe="CWE-212"
+ local cve=""
+
+ if [[ ! -s $HEADERFILE ]]; then
+ run_http_header "$1" || return 1
+ fi
+
+ # Whitelist some headers as they are mistakenly identified as ipv4 address. Issues #158, #323.
+ # Also facebook used to have a CSP rule for 127.0.0.1
+ if grep -Evai "$whitelisted_header" $HEADERFILE | grep -Eiq "$ipv4address"; then
+ pr_bold " IPv4 address in header "
+ count=0
+ while read line; do
+ result="$(grep -E "$ipv4address" <<< "$line")"
+ result=$(strip_lf "$result")
+ if [[ -n "$result" ]]; then
+ if ! $first; then
+ out "$spaces"
+ your_ip_msg=""
+ else
+ first=false
+ fi
+ pr_svrty_medium "$result"
+ outln "\n$spaces$your_ip_msg"
+ fileout "$jsonID" "MEDIUM" "$result $your_ip_msg" "$cve" "$cwe"
+ fi
+ count=$count+1
+ done < $HEADERFILE
+ fi
+}
+
+
+run_http_date() {
+ local difftime
+ local spaces=" "
+ jsonID="HTTP_clock_skew"
+
+ if [[ $SERVICE != HTTP ]] || "$CLIENT_AUTH"; then
+ return 0
+ fi
+ if [[ ! -s $HEADERFILE ]]; then
+ run_http_header "$1" || return 1
+ fi
+ pr_bold " HTTP clock skew "
+ if [[ -n "$HTTP_TIME" ]]; then
+ HTTP_TIME="$(strip_lf "$HTTP_TIME")"
+ if "$HAS_OPENBSDDATE"; then
+ # We won't normalize the date under an OpenBSD thus no subtraction is feasible
+ outln "remote: $HTTP_TIME"
+ out "${spaces}local: $(LC_ALL=C TZ=GMT date "+%a, %d %b %Y %T %Z")"
+ fileout "$jsonID" "INFO" "$HTTP_TIME - $(TZ=GMT date "+%a, %d %b %Y %T %Z")"
+ else
+ HTTP_TIME="$(parse_date "$HTTP_TIME" "+%s" "%a, %d %b %Y %T %Z" 2>>$ERRFILE)"
+ difftime=$((HTTP_TIME - NOW_TIME))
+ [[ $difftime != "-"* ]] && [[ $difftime != "0" ]] && difftime="+$difftime"
+ # process was killed, so we need to add an error
+ [[ $HAD_SLEPT -ne 0 ]] && difftime="$difftime (± 1.5)"
+ out "$difftime sec from localtime";
+ fileout "$jsonID" "INFO" "$difftime seconds from localtime"
+ fi
+ else
+ out "Got no HTTP time, maybe try different URL?";
+ fileout "$jsonID" "INFO" "Got no HTTP time, maybe try different URL?"
+ fi
+ debugme tm_out ", HTTP_TIME in epoch: $HTTP_TIME"
+ outln
+ match_ipv4_httpheader "$1"
+ return 0
+}
+
+
+# HEADERFILE needs to contain the HTTP header (made sure by invoker)
+# arg1: key=word to match
+# arg2: hint for fileout() if double header
+# arg3: indentation, i.e string w spaces
+# arg4: whether we need a CR before "misconfiguration"
+# returns:
+# 0 if header not found
+# 1-n nr of headers found, then in HEADERVALUE the first value from key
+#
+match_httpheader_key() {
+ local key="$1"
+ local spaces="$3"
+ local first=$4
+ local -i nr=0
+
+ nr=$(grep -Eaic "^ *$key:" $HEADERFILE)
+ if [[ $nr -eq 0 ]]; then
+ HEADERVALUE=""
+ return 0
+ elif [[ $nr -eq 1 ]]; then
+ HEADERVALUE="$(grep -Eia "^ *$key:" $HEADERFILE)"
+ HEADERVALUE="${HEADERVALUE#*:}" # remove leading part=key to colon
+ HEADERVALUE="$(strip_lf "$HEADERVALUE")"
+ HEADERVALUE="$(strip_leading_space "$HEADERVALUE")"
+ "$first" || out "$spaces"
+ return 1
+ else
+ "$first" || out "$spaces"
+ pr_svrty_medium "misconfiguration: "
+ pr_italic "$key"
+ pr_svrty_medium " ${nr}x"
+ outln " -- checking first one only"
+ out "$spaces"
+ HEADERVALUE="$(fgrep -Fai "$key:" $HEADERFILE | head -1)"
+ HEADERVALUE="${HEADERVALUE#*:}"
+ HEADERVALUE="$(strip_lf "$HEADERVALUE")"
+ HEADERVALUE="$(strip_leading_space "$HEADERVALUE")"
+ [[ $DEBUG -ge 2 ]] && tm_italic "$HEADERVALUE" && tm_out "\n$spaces"
+ fileout "${2}_multiple" "MEDIUM" "Multiple $2 headers. Using first header: $HEADERVALUE"
+ return $nr
+ fi
+}
+
+includeSubDomains() {
+ if grep -aiqw includeSubDomains "$1"; then
+ pr_svrty_good ", includeSubDomains"
+ return 0
+ else
+ pr_litecyan ", just this domain"
+ return 1
+ fi
+}
+
+preload() {
+ if grep -aiqw preload "$1"; then
+ pr_svrty_good ", preload"
+ return 0
+ else
+ return 1
+ fi
+}
+
+
+run_hsts() {
+ local hsts_age_sec
+ local hsts_age_days
+ local spaces=" "
+ local jsonID="HSTS"
+
+ if [[ ! -s $HEADERFILE ]]; then
+ run_http_header "$1" || return 1
+ fi
+ pr_bold " Strict Transport Security "
+ match_httpheader_key "Strict-Transport-Security" "HSTS" "$spaces" "true"
+ if [[ $? -ne 0 ]]; then
+ echo "$HEADERVALUE" >$TMPFILE
+ hsts_age_sec=$(sed -e 's/[^0-9]*//g' <<< $HEADERVALUE)
+ debugme echo "hsts_age_sec: $hsts_age_sec"
+ if [[ -n $hsts_age_sec ]]; then
+ hsts_age_days=$(( hsts_age_sec / 86400))
+ else
+ hsts_age_days=-1
+ fi
+ if [[ $hsts_age_days -eq -1 ]]; then
+ pr_svrty_medium "misconfiguration: HSTS max-age (recommended > 15552000 seconds = 180 days ) is required but missing"
+ fileout "${jsonID}_time" "MEDIUM" "misconfiguration, parameter max-age (recommended > 15552000 seconds = 180 days) missing"
+ elif [[ $hsts_age_sec -eq 0 ]]; then
+ pr_svrty_low "HSTS max-age is set to 0. HSTS is disabled"
+ fileout "${jsonID}_time" "LOW" "0. HSTS is disabled"
+ elif [[ $hsts_age_sec -ge $HSTS_MIN ]]; then
+ pr_svrty_good "$hsts_age_days days" ; out "=$hsts_age_sec s"
+ fileout "${jsonID}_time" "OK" "$hsts_age_days days (=$hsts_age_sec seconds) > $HSTS_MIN seconds"
+ else
+ pr_svrty_medium "$hsts_age_sec s = $hsts_age_days days is too short ( >= $HSTS_MIN seconds recommended)"
+ fileout "${jsonID}_time" "MEDIUM" "max-age too short. $hsts_age_days days (=$hsts_age_sec seconds) < $HSTS_MIN seconds"
+ fi
+ if includeSubDomains "$TMPFILE"; then
+ fileout "${jsonID}_subdomains" "OK" "includes subdomains"
+ else
+ fileout "${jsonID}_subdomains" "INFO" "only for this domain"
+ fi
+ if preload "$TMPFILE"; then
+ fileout "${jsonID}_preload" "OK" "domain IS marked for preloading"
+ else
+ fileout "${jsonID}_preload" "INFO" "domain is NOT marked for preloading"
+ #FIXME: To be checked against preloading lists,
+ # e.g. https://dxr.mozilla.org/mozilla-central/source/security/manager/boot/src/nsSTSPreloadList.inc
+ # https://chromium.googlesource.com/chromium/src/+/master/net/http/transport_security_state_static.json
+ fi
+ else
+ pr_svrty_low "not offered"
+ fileout "$jsonID" "LOW" "not offered"
+ fi
+ outln
+
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 0
+}
+
+
+run_hpkp() {
+ local -i hpkp_age_sec
+ local -i hpkp_age_days
+ local -i hpkp_nr_keys
+ local hpkp_spki hpkp_spki_hostcert
+ local -a backup_spki
+ local spaces=" "
+ local spaces_indented=" "
+ local certificate_found=false
+ local -i i nrsaved
+ local first_hpkp_header
+ local spki
+ local ca_hashes="$TESTSSL_INSTALL_DIR/etc/ca_hashes.txt"
+
+ if [[ ! -s $HEADERFILE ]]; then
+ run_http_header "$1" || return 1
+ fi
+ pr_bold " Public Key Pinning "
+ grep -aiw '^Public-Key-Pins' $HEADERFILE >$TMPFILE # TMPFILE includes report-only
+ if [[ $? -eq 0 ]]; then
+ if [[ $(grep -aci '^Public-Key-Pins:' $TMPFILE) -gt 1 ]]; then
+ pr_svrty_medium "Misconfiguration, multiple Public-Key-Pins headers"
+ outln ", taking first line"
+ fileout "HPKP_error" "MEDIUM" "multiple Public-Key-Pins in header"
+ first_hpkp_header="$(grep -ai '^Public-Key-Pins:' $TMPFILE | head -1)"
+ # we only evaluate the keys here, unless they a not present
+ out "$spaces "
+ elif [[ $(grep -aci '^Public-Key-Pins-Report-Only:' $TMPFILE) -gt 1 ]]; then
+ outln "Multiple HPKP headers (Report-Only), taking first line"
+ fileout "HPKP_notice" "INFO" "multiple Public-Key-Pins-Report-Only in header"
+ first_hpkp_header="$(grep -ai '^Public-Key-Pins-Report-Only:' $TMPFILE | head -1)"
+ out "$spaces "
+ elif [[ $(grep -Eaci '^Public-Key-Pins:|^Public-Key-Pins-Report-Only:' $TMPFILE) -eq 2 ]]; then
+ outln "Public-Key-Pins + Public-Key-Pins-Report-Only detected. Continue with first one"
+ first_hpkp_header="$(grep -ai '^Public-Key-Pins:' $TMPFILE)"
+ out "$spaces "
+ elif [[ $(grep -aci '^Public-Key-Pins:' $TMPFILE) -eq 1 ]]; then
+ first_hpkp_header="$(grep -ai '^Public-Key-Pins:' $TMPFILE)"
+ else
+ outln "Public-Key-Pins-Only detected"
+ first_hpkp_header="$(grep -ai '^Public-Key-Pins-Report-Only:' $TMPFILE)"
+ out "$spaces "
+ fileout "HPKP_SPKIs" "INFO" "Only Public-Key-Pins-Report-Only"
+ fi
+
+ # remove leading Public-Key-Pins* and convert it to multiline arg
+ sed -e 's/Public-Key-Pins://g' -e s'/Public-Key-Pins-Report-Only://' <<< "$first_hpkp_header" | \
+ tr ';' '\n' | sed -e 's/\"//g' -e 's/^ //' >$TMPFILE
+
+ hpkp_nr_keys=$(grep -ac pin-sha $TMPFILE)
+ if [[ $hpkp_nr_keys -eq 1 ]]; then
+ pr_svrty_high "Only one key pinned (NOT ok), means the site may become unavailable in the future, "
+ fileout "HPKP_SPKIs" "HIGH" "Only one key pinned"
+ else
+ pr_svrty_good "$hpkp_nr_keys"
+ out " keys, "
+ fileout "HPKP_SPKIs" "OK" "$hpkp_nr_keys keys pinned in header"
+ fi
+
+ # print key=value pair with awk, then strip non-numbers, to be improved with proper parsing of key-value with awk
+ if "$HAS_SED_E"; then
+ hpkp_age_sec=$(awk -F= '/max-age/{max_age=$2; print max_age}' $TMPFILE | sed -E 's/[^[:digit:]]//g')
+ else
+ hpkp_age_sec=$(awk -F= '/max-age/{max_age=$2; print max_age}' $TMPFILE | sed -r 's/[^[:digit:]]//g')
+ fi
+ hpkp_age_days=$((hpkp_age_sec / 86400))
+ if [[ $hpkp_age_sec -ge $HPKP_MIN ]]; then
+ pr_svrty_good "$hpkp_age_days days" ; out "=$hpkp_age_sec s"
+ fileout "HPKP_age" "OK" "HPKP age is set to $hpkp_age_days days ($hpkp_age_sec sec)"
+ else
+ out "$hpkp_age_sec s = "
+ pr_svrty_medium "$hpkp_age_days days (< $HPKP_MIN s = $((HPKP_MIN / 86400)) days is not good enough)"
+ fileout "HPKP_age" "MEDIUM" "age is set to $hpkp_age_days days ($hpkp_age_sec sec) < $HPKP_MIN s = $((HPKP_MIN / 86400)) days is not good enough."
+ fi
+
+ if includeSubDomains "$TMPFILE"; then
+ fileout "HPKP_subdomains" "INFO" "is valid for subdomains as well"
+ else
+ fileout "HPKP_subdomains" "INFO" "is valid for this domain only"
+ fi
+ if preload "$TMPFILE"; then
+ fileout "HPKP_preload" "INFO" "IS marked for browser preloading"
+ else
+ fileout "HPKP_preload" "INFO" "NOT marked for browser preloading"
+ fi
+
+ # Get the SPKIs first
+ spki=$(tr ';' '\n' < $TMPFILE | tr -d ' ' | tr -d '\"' | awk -F'=' '/pin.*=/ { print $2 }')
+ debugme tmln_out "\n$spki"
+
+ # Look at the host certificate first
+ if [[ ! -s "$HOSTCERT" ]]; then
+ get_host_cert || return 1
+ # no host certificate
+ fi
+
+ hpkp_spki_hostcert="$($OPENSSL x509 -in $HOSTCERT -pubkey -noout 2>/dev/null | grep -v PUBLIC | \
+ $OPENSSL base64 -d 2>/dev/null | $OPENSSL dgst -sha256 -binary 2>/dev/null | $OPENSSL base64 2>/dev/null)"
+ hpkp_ca="$($OPENSSL x509 -in $HOSTCERT -issuer -noout 2>/dev/null |sed 's/^.*CN=//' | sed 's/\/.*$//')"
+
+ # Get keys/hashes from intermediate certificates
+ $OPENSSL s_client $(s_client_options "$STARTTLS $BUGS $PROXY -showcerts -connect $NODEIP:$PORT $SNI") </dev/null >$TMPFILE 2>$ERRFILE
+ # Place the server's certificate in $HOSTCERT and any intermediate
+ # certificates that were provided in $TEMPDIR/intermediatecerts.pem
+ # https://backreference.org/2010/05/09/ocsp-verification-with-openssl/
+ awk -v n=-1 "/Certificate chain/ {start=1}
+ /-----BEGIN CERTIFICATE-----/{ if (start) {inc=1; n++} }
+ inc { print > (\"$TEMPDIR/level\" n \".crt\") }
+ /---END CERTIFICATE-----/{ inc=0 }" $TMPFILE
+ nrsaved=$(count_words "$(echo $TEMPDIR/level?.crt 2>/dev/null)")
+ rm $TEMPDIR/level0.crt 2>/dev/null
+
+ printf ""> "$TEMPDIR/intermediate.hashes"
+ if [[ $nrsaved -ge 2 ]]; then
+ for cert_fname in $TEMPDIR/level?.crt; do
+ hpkp_spki_ca="$($OPENSSL x509 -in "$cert_fname" -pubkey -noout 2>/dev/null | grep -v PUBLIC | $OPENSSL base64 -d 2>/dev/null |
+ $OPENSSL dgst -sha256 -binary 2>/dev/null | $OPENSSL enc -base64 2>/dev/null)"
+ hpkp_name="$(get_cn_from_cert $cert_fname)"
+ hpkp_ca="$($OPENSSL x509 -in $cert_fname -issuer -noout 2>/dev/null |sed 's/^.*CN=//' | sed 's/\/.*$//')"
+ [[ -n $hpkp_name ]] || hpkp_name=$($OPENSSL x509 -in "$cert_fname" -subject -noout 2>/dev/null | sed 's/^subject= //')
+ echo "$hpkp_spki_ca $hpkp_name" >> "$TEMPDIR/intermediate.hashes"
+ done
+ fi
+
+ # This is where the matching magic starts. First host, intermediate, then root certificate from the supplied stores
+ spki_match=false
+ has_backup_spki=false
+ i=0
+ for hpkp_spki in $spki; do
+ certificate_found=false
+ # compare collected SPKIs against the host certificate
+ if [[ "$hpkp_spki_hostcert" == "$hpkp_spki" ]] || [[ "$hpkp_spki_hostcert" == "$hpkp_spki=" ]]; then
+ certificate_found=true # We have a match
+ spki_match=true
+ out "\n$spaces_indented Host cert: "
+ pr_svrty_good "$hpkp_spki"
+ fileout "HPKP_$hpkp_spki" "OK" "SPKI $hpkp_spki matches the host certificate"
+ fi
+ debugme tm_out "\n $hpkp_spki | $hpkp_spki_hostcert"
+
+ # Check for intermediate match
+ if ! "$certificate_found"; then
+ hpkp_matches=$(grep "$hpkp_spki" $TEMPDIR/intermediate.hashes 2>/dev/null)
+ if [[ -n $hpkp_matches ]]; then # hpkp_matches + hpkp_spki + '='
+ # We have a match
+ certificate_found=true
+ spki_match=true
+ out "\n$spaces_indented Sub CA: "
+ pr_svrty_good "$hpkp_spki"
+ ca_cn="$(sed "s/^[a-zA-Z0-9\+\/]*=* *//" <<< $"$hpkp_matches" )"
+ pr_italic " $ca_cn"
+ fileout "HPKP_$hpkp_spki" "OK" "SPKI $hpkp_spki matches Intermediate CA \"$ca_cn\" pinned in the HPKP header"
+ fi
+ fi
+
+ # we compare now against a precompiled list of SPKIs against the ROOT CAs we have in $ca_hashes
+ if ! "$certificate_found"; then
+ hpkp_matches=$(grep -h "$hpkp_spki" $ca_hashes 2>/dev/null | sort -u)
+ if [[ -n $hpkp_matches ]]; then
+ certificate_found=true # root CA found
+ spki_match=true
+ if [[ $(count_lines "$hpkp_matches") -eq 1 ]]; then
+ # replace by awk
+ match_ca=$(sed "s/[a-zA-Z0-9\+\/]*=* *//" <<< "$hpkp_matches")
+ else
+ match_ca=""
+
+ fi
+ ca_cn="$(sed "s/^[a-zA-Z0-9\+\/]*=* *//" <<< $"$hpkp_matches" )"
+ if [[ "$match_ca" == "$hpkp_ca" ]]; then # part of the chain
+ out "\n$spaces_indented Root CA: "
+ pr_svrty_good "$hpkp_spki"
+ pr_italic " $ca_cn"
+ fileout "HPKP_$hpkp_spki" "INFO" "SPKI $hpkp_spki matches Root CA \"$ca_cn\" pinned. (Root CA part of the chain)"
+ else # not part of chain
+ match_ca=""
+ has_backup_spki=true # Root CA outside the chain --> we save it for unmatched
+ fileout "HPKP_$hpkp_spki" "INFO" "SPKI $hpkp_spki matches Root CA \"$ca_cn\" pinned. (Root backup SPKI)"
+ backup_spki[i]="$(strip_lf "$hpkp_spki")" # save it for later
+ backup_spki_str[i]="$ca_cn" # also the name=CN of the root CA
+ i=$((i + 1))
+ fi
+ fi
+ fi
+
+ # still no success --> it's probably a backup SPKI
+ if ! "$certificate_found"; then
+ # Most likely a backup SPKI, unfortunately we can't tell for what it is: host, intermediates
+ has_backup_spki=true
+ backup_spki[i]="$(strip_lf "$hpkp_spki")" # save it for later
+ backup_spki_str[i]="" # no root ca
+ i=$((i + 1))
+ fileout "HPKP_$hpkp_spki" "INFO" "SPKI $hpkp_spki doesn't match anything. This is ok for a backup for any certificate"
+ # CSV/JSON output here for the sake of simplicity, rest we do en bloc below
+ fi
+ done
+
+ # now print every backup spki out we saved before
+ out "\n$spaces_indented Backups: "
+
+ # for i=0 manually do the same as below as there's other indentation here
+ if [[ -n "${backup_spki_str[0]}" ]]; then
+ pr_svrty_good "${backup_spki[0]}"
+ #out " Root CA: "
+ prln_italic " ${backup_spki_str[0]}"
+ else
+ outln "${backup_spki[0]}"
+ fi
+ # now for i=1
+ for ((i=1; i < ${#backup_spki[@]} ;i++ )); do
+ if [[ -n "${backup_spki_str[i]}" ]]; then
+ # it's a Root CA outside the chain
+ pr_svrty_good "$spaces_indented ${backup_spki[i]}"
+ #out " Root CA: "
+ prln_italic " ${backup_spki_str[i]}"
+ else
+ outln "$spaces_indented ${backup_spki[i]}"
+ fi
+ done
+ if [[ ! -f "$ca_hashes" ]] && "$spki_match"; then
+ out "$spaces "
+ prln_warning "Attribution of further hashes couldn't be done as $ca_hashes could not be found"
+ fileout "HPKP_SPKImatch" "WARN" "Attribution of further hashes possible as $ca_hashes could not be found"
+ fi
+
+ # If all else fails...
+ if ! "$spki_match"; then
+ "$has_backup_spki" && out "$spaces" # we had a few lines with backup SPKIs already
+ prln_svrty_high " No matching key for SPKI found "
+ fileout "HPKP_SPKImatch" "HIGH" "None of the SPKI match your host certificate, intermediate CA or known root CAs. Bricked site?"
+ fi
+
+ if ! "$has_backup_spki"; then
+ prln_svrty_high " No backup keys found. Loss/compromise of the currently pinned key(s) will lead to bricked site. "
+ fileout "HPKP_backup" "HIGH" "No backup keys found. Loss/compromise of the currently pinned key(s) will lead to bricked site."
+ fi
+ else
+ outln "--"
+ fileout "HPKP" "INFO" "No support for HTTP Public Key Pinning"
+ fi
+
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 0
+}
+
+emphasize_stuff_in_headers(){
+ local html_brown="<span style=\\\"color:olive;\\\">"
+ local html_yellow="<span style=\\\"color:olive;font-weight:bold;\\\">"
+ local html_off="<\\/span>"
+
+# see https://www.grymoire.com/Unix/Sed.html#uh-3
+# outln "$1" | sed "s/[0-9]*/$brown&${off}/g"
+ tmln_out "$1" | sed -e "s/\([0-9]\)/${brown}\1${off}/g" \
+ -e "s/Unix/${yellow}Unix${off}/g" \
+ -e "s/Debian/${yellow}Debian${off}/g" \
+ -e "s/Win32/${yellow}Win32${off}/g" \
+ -e "s/Win64/${yellow}Win64${off}/g" \
+ -e "s/Ubuntu/${yellow}Ubuntu${off}/g" \
+ -e "s/ubuntu/${yellow}ubuntu${off}/g" \
+ -e "s/buster/${yellow}buster${off}/g" \
+ -e "s/stretch/${yellow}stretch${off}/g" \
+ -e "s/jessie/${yellow}jessie${off}/g" \
+ -e "s/squeeze/${yellow}squeeze${off}/g" \
+ -e "s/wheezy/${yellow}wheezy${off}/g" \
+ -e "s/lenny/${yellow}lenny${off}/g" \
+ -e "s/SUSE/${yellow}SUSE${off}/g" \
+ -e "s/Red Hat Enterprise Linux/${yellow}Red Hat Enterprise Linux${off}/g" \
+ -e "s/Red Hat/${yellow}Red Hat${off}/g" \
+ -e "s/CentOS/${yellow}CentOS${off}/g" \
+ -e "s/Via/${yellow}Via${off}/g" \
+ -e "s/X-Forwarded/${yellow}X-Forwarded${off}/g" \
+ -e "s/Liferay-Portal/${yellow}Liferay-Portal${off}/g" \
+ -e "s/X-Cache-Lookup/${yellow}X-Cache-Lookup${off}/g" \
+ -e "s/X-Cache/${yellow}X-Cache${off}/g" \
+ -e "s/X-Squid/${yellow}X-Squid${off}/g" \
+ -e "s/X-Server/${yellow}X-Server${off}/g" \
+ -e "s/X-Varnish/${yellow}X-Varnish${off}/g" \
+ -e "s/X-OWA-Version/${yellow}X-OWA-Version${off}/g" \
+ -e "s/MicrosoftSharePointTeamServices/${yellow}MicrosoftSharePointTeamServices${off}/g" \
+ -e "s/X-Application-Context/${yellow}X-Application-Context${off}/g" \
+ -e "s/X-Version/${yellow}X-Version${off}/g" \
+ -e "s/X-Powered-By/${yellow}X-Powered-By${off}/g" \
+ -e "s/X-UA-Compatible/${yellow}X-UA-Compatible${off}/g" \
+ -e "s/Link/${yellow}Link${off}/g" \
+ -e "s/X-Rack-Cache/${yellow}X-Rack-Cache${off}/g" \
+ -e "s/X-Runtime/${yellow}X-Runtime${off}/g" \
+ -e "s/X-Pingback/${yellow}X-Pingback${off}/g" \
+ -e "s/X-Permitted-Cross-Domain-Policies/${yellow}X-Permitted-Cross-Domain-Policies${off}/g" \
+ -e "s/X-AspNet-Version/${yellow}X-AspNet-Version${off}/g" \
+ -e "s/x-note/${yellow}x-note${off}/g" \
+ -e "s/x-global-transaction-id/${yellow}x-global-transaction-id${off}/g" \
+ -e "s/X-Global-Transaction-ID/${yellow}X-Global-Transaction-ID${off}/g" \
+ -e "s/Alt-Svc/${yellow}Alt-Svc${off}/g" \
+ -e "s/system-wsgw-management-loopback/${yellow}system-wsgw-management-loopback${off}/g"
+
+ if "$do_html"; then
+ if [[ $COLOR -ge 2 ]]; then
+ html_out "$(tm_out "$1" | sed -e 's/\&/\&amp;/g' \
+ -e 's/</\&lt;/g' -e 's/>/\&gt;/g' -e 's/"/\&quot;/g' -e "s/'/\&apos;/g" \
+ -e "s/\([0-9]\)/${html_brown}\1${html_off}/g" \
+ -e "s/Unix/${html_yellow}Unix${html_off}/g" \
+ -e "s/Debian/${html_yellow}Debian${html_off}/g" \
+ -e "s/Win32/${html_yellow}Win32${html_off}/g" \
+ -e "s/Win64/${html_yellow}Win64${html_off}/g" \
+ -e "s/Ubuntu/${html_yellow}Ubuntu${html_off}/g" \
+ -e "s/ubuntu/${html_yellow}ubuntu${html_off}/g" \
+ -e "s/buster/${html_yellow}buster${html_off}/g" \
+ -e "s/stretch/${html_yellow}stretch${html_off}/g" \
+ -e "s/jessie/${html_yellow}jessie${html_off}/g" \
+ -e "s/squeeze/${html_yellow}squeeze${html_off}/g" \
+ -e "s/wheezy/${html_yellow}wheezy${html_off}/g" \
+ -e "s/lenny/${html_yellow}lenny${html_off}/g" \
+ -e "s/SUSE/${html_yellow}SUSE${html_off}/g" \
+ -e "s/Red Hat Enterprise Linux/${html_yellow}Red Hat Enterprise Linux${html_off}/g" \
+ -e "s/Red Hat/${html_yellow}Red Hat${html_off}/g" \
+ -e "s/CentOS/${html_yellow}CentOS${html_off}/g" \
+ -e "s/Via/${html_yellow}Via${html_off}/g" \
+ -e "s/X-Forwarded/${html_yellow}X-Forwarded${html_off}/g" \
+ -e "s/Liferay-Portal/${html_yellow}Liferay-Portal${html_off}/g" \
+ -e "s/X-Cache-Lookup/${html_yellow}X-Cache-Lookup${html_off}/g" \
+ -e "s/X-Cache/${html_yellow}X-Cache${html_off}/g" \
+ -e "s/X-Squid/${html_yellow}X-Squid${html_off}/g" \
+ -e "s/X-Server/${html_yellow}X-Server${html_off}/g" \
+ -e "s/X-Varnish/${html_yellow}X-Varnish${html_off}/g" \
+ -e "s/X-OWA-Version/${html_yellow}X-OWA-Version${html_off}/g" \
+ -e "s/MicrosoftSharePointTeamServices/${html_yellow}MicrosoftSharePointTeamServices${html_off}/g" \
+ -e "s/X-Application-Context/${html_yellow}X-Application-Context${html_off}/g" \
+ -e "s/X-Version/${html_yellow}X-Version${html_off}/g" \
+ -e "s/X-Powered-By/${html_yellow}X-Powered-By${html_off}/g" \
+ -e "s/X-UA-Compatible/${html_yellow}X-UA-Compatible${html_off}/g" \
+ -e "s/Link/${html_yellow}Link${html_off}/g" \
+ -e "s/X-Runtime/${html_yellow}X-Runtime${html_off}/g" \
+ -e "s/X-Rack-Cache/${html_yellow}X-Rack-Cache${html_off}/g" \
+ -e "s/X-Pingback/${html_yellow}X-Pingback${html_off}/g" \
+ -e "s/X-Permitted-Cross-Domain-Policies/${html_yellow}X-Permitted-Cross-Domain-Policies${html_off}/g" \
+ -e "s/X-AspNet-Version/${html_yellow}X-AspNet-Version${html_off}/g")" \
+ -e "s/x-note/${html_yellow}x-note${html_off}/g" \
+ -e "s/X-Global-Transaction-ID/${html_yellow}X-Global-Transaction-ID${html_off}/g" \
+ -e "s/x-global-transaction-id/${html_yellow}x-global-transaction-id${html_off}/g" \
+ -e "s/Alt-Svc/${html_yellow}Alt-Svc${html_off}/g" \
+ -e "s/system-wsgw-management-loopback/${html_yellow}system-wsgw-management-loopback${html_off}/g"
+#FIXME: this is double code. The pattern to emphasize would fit better into
+# one function.
+# Also we need another function like run_other_header as otherwise "Link" "Alt-Svc" will never be found.
+# And: I matches case sensitive only which might not detect all banners. (sed ignorecase is not possible w/ BSD sed)
+ else
+ html_out "$(html_reserved "$1")"
+ fi
+ html_out "\n"
+ fi
+}
+
+run_server_banner() {
+ local serverbanner
+ local jsonID="banner_server"
+
+ if [[ ! -s $HEADERFILE ]]; then
+ run_http_header "$1" || return 1
+ fi
+ pr_bold " Server banner "
+ grep -ai '^Server' $HEADERFILE >$TMPFILE
+ if [[ $? -eq 0 ]]; then
+ serverbanner=$(sed -e 's/^Server: //' -e 's/^server: //' $TMPFILE)
+ if [[ "$serverbanner" == $'\n' ]] || [[ "$serverbanner" == $'\r' ]] || [[ "$serverbanner" == $'\n\r' ]] || [[ -z "$serverbanner" ]]; then
+ outln "exists but empty string"
+ fileout "$jsonID" "INFO" "Server banner is empty"
+ else
+ emphasize_stuff_in_headers "$serverbanner"
+ fileout "$jsonID" "INFO" "$serverbanner"
+ if [[ "$serverbanner" == *Microsoft-IIS/6.* ]] && [[ $OSSL_VER == 1.0.2* ]]; then
+ prln_warning " It's recommended to run another test w/ OpenSSL 1.0.1 !"
+ # see https://github.com/PeterMosmans/openssl/issues/19#issuecomment-100897892
+ fileout "${jsonID}" "WARN" "IIS6_openssl_mismatch: Recommended to rerun this test w/ OpenSSL 1.0.1. See https://github.com/PeterMosmans/openssl/issues/19#issuecomment-100897892"
+ fi
+ fi
+ # mozilla.github.io/server-side-tls/ssl-config-generator/
+ # https://support.microsoft.com/en-us/kb/245030
+ else
+ outln "(no \"Server\" line in header, interesting!)"
+ fileout "$jsonID" "INFO" "No Server banner line in header, interesting!"
+ fi
+
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 0
+}
+
+run_appl_banner() {
+ local line
+ local first=true
+ local spaces=" "
+ local appl_banners=""
+ local jsonID="banner_application"
+
+ if [[ ! -s $HEADERFILE ]]; then
+ run_http_header "$1" || return 1
+ fi
+ pr_bold " Application banner "
+ grep -Eai '^X-Powered-By|^X-AspNet-Version|^X-Version|^Liferay-Portal|^X-OWA-Version^|^MicrosoftSharePointTeamServices' $HEADERFILE >$TMPFILE
+ if [[ $? -ne 0 ]]; then
+ outln "--"
+ fileout "$jsonID" "INFO" "No application banner found"
+ else
+ while IFS='' read -r line; do
+ line=$(strip_lf "$line")
+ if ! $first; then
+ out "$spaces"
+ appl_banners="${appl_banners}, ${line}"
+ else
+ appl_banners="${line}"
+ first=false
+ fi
+ emphasize_stuff_in_headers "$line"
+ done < "$TMPFILE"
+ fileout "$jsonID" "INFO" "$appl_banners"
+ fi
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 0
+}
+
+run_rp_banner() {
+ local line
+ local first=true
+ local spaces=" "
+ local rp_banners=""
+ local jsonID="banner_reverseproxy"
+ local cwe="CWE-200"
+ local cve=""
+
+ if [[ ! -s $HEADERFILE ]]; then
+ run_http_header "$1" || return 1
+ fi
+ pr_bold " Reverse Proxy banner "
+ grep -Eai '^Via:|^X-Cache|^X-Squid|^X-Varnish:|^X-Server-Name:|^X-Server-Port:|^x-forwarded|^Forwarded' $HEADERFILE >$TMPFILE
+ if [[ $? -ne 0 ]]; then
+ outln "--"
+ fileout "$jsonID" "INFO" "--" "$cve" "$cwe"
+ else
+ while read line; do
+ line=$(strip_lf "$line")
+ if $first; then
+ first=false
+ else
+ out "$spaces"
+ fi
+ emphasize_stuff_in_headers "$line"
+ rp_banners="${rp_banners}${line}"
+ done < $TMPFILE
+ fileout "$jsonID" "INFO" "$rp_banners" "$cve" "$cwe"
+ fi
+ outln
+
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 0
+}
+
+
+# arg1: multiline string w cookies
+#
+sub_f5_bigip_check() {
+ local allcookies="$1"
+ local ip port cookievalue cookiename
+ local routed_domain offset
+ local savedcookies=""
+ local spaces="$2"
+ local cwe="CWE-212"
+ local cve=""
+
+ # taken from https://github.com/drwetter/F5-BIGIP-Decoder, more details see there
+
+ debugme echo -e "all cookies: >> $allcookies <<\n"
+ while true; do IFS='=' read cookiename cookievalue
+ [[ -z "$cookievalue" ]] && break
+ cookievalue=${cookievalue/;/}
+ debugme echo $cookiename : $cookievalue
+ if grep -Eq '[0-9]{9,10}\.[0-9]{3,5}\.0000' <<< "$cookievalue"; then
+ ip="$(f5_ip_oldstyle "$cookievalue")"
+ port="$(f5_port_decode $cookievalue)"
+ out "${spaces}F5 cookie (default IPv4 pool member): "; pr_italic "$cookiename "; prln_svrty_medium "${ip}:${port}"
+ fileout "cookie_bigip_f5" "MEDIUM" "Information leakage: F5 cookie $cookiename $cookievalue is default IPv4 pool member ${ip}:${port}" "$cve" "$cwe"
+ elif grep -Eq '^rd[0-9]{1,3}o0{20}f{4}[a-f0-9]{8}o[0-9]{1,5}' <<< "$cookievalue"; then
+ routed_domain="$(f5_determine_routeddomain "$cookievalue")"
+ offset=$(( 2 + ${#routed_domain} + 1 + 24))
+ port="${cookievalue##*o}"
+ ip="$(f5_hex2ip "${cookievalue:$offset:8}")"
+ out "${spaces}F5 cookie (IPv4 pool in routed domain "; pr_svrty_medium "$routed_domain"; out "): "; pr_italic "$cookiename "; prln_svrty_medium "${ip}:${port}"
+ fileout "cookie_bigip_f5" "MEDIUM" "Information leakage: F5 cookie $cookiename $cookievalue is IPv4 pool member in routed domain $routed_domain ${ip}:${port}" "$cve" "$cwe"
+ elif grep -Eq '^vi[a-f0-9]{32}\.[0-9]{1,5}' <<< "$cookievalue"; then
+ ip="$(f5_hex2ip6 ${cookievalue:2:32})"
+ port="${cookievalue##*.}"
+ port=$(f5_port_decode "$port")
+ out "${spaces}F5 cookie (default IPv6 pool member): "; pr_italic "$cookiename "; prln_svrty_medium "${ip}:${port}"
+ fileout "cookie_bigip_f5" "MEDIUM" "Information leakage: F5 cookie $cookiename $cookievalue is default IPv6 pool member ${ip}:${port}" "$cve" "$cwe"
+ elif grep -Eq '^rd[0-9]{1,3}o[a-f0-9]{32}o[0-9]{1,5}' <<< "$cookievalue"; then
+ routed_domain="$(f5_determine_routeddomain "$cookievalue")"
+ offset=$(( 2 + ${#routed_domain} + 1 ))
+ port="${cookievalue##*o}"
+ ip="$(f5_hex2ip6 ${cookievalue:$offset:32})"
+ out "${spaces}F5 cookie (IPv6 pool in routed domain "; pr_svrty_medium "$routed_domain"; out "): "; pr_italic "$cookiename "; prln_svrty_medium "${ip}:${port}"
+ fileout "cookie_bigip_f5" "MEDIUM" "Information leakage: F5 cookie $cookiename $cookievalue is IPv6 pool member in routed domain $routed_domain ${ip}:${port}" "$cve" "$cwe"
+ elif grep -Eq '^\!.*=$' <<< "$cookievalue"; then
+ if [[ "${#cookievalue}" -eq 81 ]] ; then
+ savedcookies="${savedcookies} ${cookiename}=${cookievalue:1:79}"
+ out "${spaces}Encrypted F5 cookie named "; pr_italic "${cookiename}"; outln " detected"
+ fileout "cookie_bigip_f5" "INFO" "encrypted F5 cookie named ${cookiename}"
+ fi
+ fi
+ done <<< "$allcookies"
+}
+
+
+run_cookie_flags() { # ARG1: Path
+ local -i nr_cookies
+ local -i nr_httponly nr_secure
+ local negative_word
+ local msg302="" msg302_=""
+ local spaces=" "
+
+ if [[ ! -s $HEADERFILE ]]; then
+ run_http_header "$1" || return 1
+ fi
+
+ if [[ ! "$HTTP_STATUS_CODE" =~ 20 ]]; then
+ if [[ "$HTTP_STATUS_CODE" =~ [301|302] ]]; then
+ msg302=" -- maybe better try target URL of 30x"
+ msg302_=" (30x detected, better try target URL of 30x)"
+ else
+ msg302=" -- HTTP status $HTTP_STATUS_CODE signals you maybe missed the web application"
+ msg302_=" (maybe missed the application)"
+ fi
+ fi
+
+ pr_bold " Cookie(s) "
+ grep -ai '^Set-Cookie' $HEADERFILE >$TMPFILE
+ if [[ $? -ne 0 ]]; then
+ outln "(none issued at \"$1\")$msg302"
+ fileout "cookie_count" "INFO" "0 at \"$1\"$msg302_"
+ else
+ nr_cookies=$(count_lines "$(cat $TMPFILE)")
+ out "$nr_cookies issued: "
+ fileout "cookie_count" "INFO" "$nr_cookies at \"$1\"$msg302_"
+ if [[ $nr_cookies -gt 1 ]]; then
+ negative_word="NONE"
+ else
+ negative_word="NOT"
+ fi
+ nr_secure=$(grep -iac secure $TMPFILE)
+ case $nr_secure in
+ 0) pr_svrty_medium "$negative_word" ;;
+ [123456789]) pr_svrty_good "$nr_secure/$nr_cookies";;
+ esac
+ out " secure, "
+ if [[ $nr_cookies -eq $nr_secure ]]; then
+ fileout "cookie_secure" "OK" "All ($nr_cookies) at \"$1\" marked as secure"
+ else
+ fileout "cookie_secure" "INFO" "$nr_secure/$nr_cookies at \"$1\" marked as secure"
+ fi
+ nr_httponly=$(grep -cai httponly $TMPFILE)
+ case $nr_httponly in
+ 0) pr_svrty_medium "$negative_word" ;;
+ [123456789]) pr_svrty_good "$nr_httponly/$nr_cookies";;
+ esac
+ out " HttpOnly"
+ if [[ $nr_cookies -eq $nr_httponly ]]; then
+ fileout "cookie_httponly" "OK" "All ($nr_cookies) at \"$1\" marked as HttpOnly$msg302_"
+ else
+ fileout "cookie_httponly" "INFO" "$nr_secure/$nr_cookies at \"$1\" marked as HttpOnly$msg302_"
+ fi
+ outln "$msg302"
+ allcookies="$(awk '/[Ss][Ee][Tt]-[Cc][Oo][Oo][Kk][Ii][Ee]:/ { print $2 }' "$TMPFILE")"
+ sub_f5_bigip_check "$allcookies" "$spaces"
+ fi
+
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 0
+}
+
+
+run_security_headers() {
+ local good_header="X-Frame-Options X-Content-Type-Options Content-Security-Policy X-Content-Security-Policy X-WebKit-CSP Content-Security-Policy-Report-Only Expect-CT"
+ local other_header="Access-Control-Allow-Origin Upgrade X-Served-By Referrer-Policy X-UA-Compatible Cache-Control Pragma X-XSS-Protection"
+ local header header_output
+ local first=true
+ local spaces=" "
+ local have_header=false
+
+ if [[ ! -s $HEADERFILE ]]; then
+ run_http_header "$1" || return 1
+ fi
+
+ pr_bold " Security headers "
+ for header in $good_header; do
+ [[ "$DEBUG" -ge 5 ]] && echo "testing \"$header\""
+ match_httpheader_key "$header" "$header" "$spaces" "$first"
+ if [[ $? -ge 1 ]]; then
+ have_header=true
+ if "$first"; then
+ first=false
+ fi
+ # Include $header when determining where to insert line breaks, but print $header
+ # separately.
+ pr_svrty_good "$header"; out ":"
+ header_output="$(out_row_aligned_max_width "${header:2} $HEADERVALUE" "$spaces " $TERM_WIDTH)"
+ outln "${header_output#${header:2}}"
+ fileout "$header" "OK" "$HEADERVALUE"
+ fi
+ done
+
+ for header in $other_header; do
+ [[ "$DEBUG" -ge 5 ]] && echo "testing \"$header\""
+ match_httpheader_key "$header" "$header" "$spaces" "$first"
+ if [[ $? -ge 1 ]]; then
+ have_header=true
+ if "$first"; then
+ first=false
+ fi
+ out "$header"
+ outln ": $HEADERVALUE" # shouldn't be that long
+ fileout "$header" "INFO" "$HEADERVALUE"
+ fi
+ done
+ #TODO: I am not testing for the correctness or anything stupid yet, e.g. "X-Frame-Options: allowall" or Access-Control-Allow-Origin: *
+
+ if ! "$have_header"; then
+ prln_svrty_medium "--"
+ fileout "security_headers" "MEDIUM" "--"
+ fi
+
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 0
+}
+
+
+# #1: string with 2 opensssl codes, output is same in NSS/ssllabs terminology
+normalize_ciphercode() {
+ if [[ "${1:2:2}" == "00" ]]; then
+ tm_out "$(tolower "x${1:7:2}")"
+ else
+ tm_out "$(tolower "x${1:2:2}${1:7:2}${1:12:2}")"
+ fi
+ return 0
+}
+
+prettyprint_local() {
+ local arg line
+ local hexc hexcode dash ciph sslvers kx auth enc mac export
+ local re='^[0-9A-Fa-f]+$'
+
+ if [[ "$1" == 0x* ]] || [[ "$1" == 0X* ]]; then
+ fatal "pls supply x<number> instead" $ERR_CMDLINE
+ fi
+
+ if [[ -z "$1" ]]; then
+ pr_headline " Displaying all $OPENSSL_NR_CIPHERS local ciphers ";
+ else
+ pr_headline " Displaying all local ciphers ";
+ # pattern provided; which one?
+ [[ $1 =~ $re ]] && \
+ pr_headline "matching number pattern \"$1\" " || \
+ pr_headline "matching word pattern "\"$1\"" (ignore case) "
+ fi
+ outln "\n"
+ neat_header
+
+ if [[ -z "$1" ]]; then
+ actually_supported_osslciphers 'ALL:COMPLEMENTOFALL:@STRENGTH' 'ALL' "-V" | while read -r hexcode dash ciph sslvers kx auth enc mac export ; do # -V doesn't work with openssl < 1.0
+ hexc="$(normalize_ciphercode $hexcode)"
+ outln "$(neat_list "$hexc" "$ciph" "$kx" "$enc")"
+ done
+ else
+ #for arg in $(echo $@ | sed 's/,/ /g'); do
+ for arg in ${*//,/ /}; do
+ actually_supported_osslciphers 'ALL:COMPLEMENTOFALL:@STRENGTH' 'ALL' "-V" | while read -r hexcode dash ciph sslvers kx auth enc mac export ; do # -V doesn't work with openssl < 1.0
+ hexc="$(normalize_ciphercode $hexcode)"
+ # for numbers we don't do word matching:
+ [[ $arg =~ $re ]] && \
+ line="$(neat_list "$hexc" "$ciph" "$kx" "$enc" | grep -ai "$arg")" || \
+ line="$(neat_list "$hexc" "$ciph" "$kx" "$enc" | grep -wai "$arg")"
+ [[ -n "$line" ]] && outln "$line"
+ done
+ done
+ fi
+ outln
+ return 0
+}
+
+
+# Generic function for a rated output, no used yet.
+# arg1: rating from 2 to -4 if available or not
+# arg2: no/yes: decides whether positive or negative logic will be applied and "not" will be printed
+# arg3: jsonID
+#
+rated_output() {
+ local jsonID=$3
+ local logic=""
+
+ if [[ $2 == no ]] || [[ $2 == negative ]]; then
+ logic="not "
+ fi
+ case $1 in
+ 2) pr_svrty_best "${logic}offered (OK)"
+ fileout "${jsonID}" "OK" "${logic}offered"
+ ;;
+ 1) pr_svrty_good "${logic}offered (OK)"
+ fileout "${jsonID}" "OK" "${logic}offered"
+ ;;
+ 0) out "${logic}offered"
+ fileout "${jsonID}" "INFO" "${logic}offered"
+ ;;
+ -1) pr_svrty_low "${logic}offered"
+ fileout "${jsonID}" "LOW" "${logic}offered"
+ ;;
+ -2) pr_svrty_medium "${logic}offered"
+ fileout "${jsonID}" "MEDIUM" "${logic}offered"
+ ;;
+ -3) pr_svrty_high "${logic}offered (NOT ok)"
+ fileout "${jsonID}" "HIGH" "${logic}offered"
+ ;;
+ -4) pr_svrty_critical "${logic}offered (NOT ok)"
+ fileout "${jsonID}" "CRITICAL" "${logic}offered"
+ ;;
+ *) pr_warning "FIXME: error around $LINENO, (please report this)"
+ fileout "${jsonID}" "WARN" "return condition $2 when $1 unclear"
+ return 1
+ ;;
+ esac
+ return 0
+}
+
+
+openssl2rfc() {
+ local rfcname=""
+ local -i i
+
+ for (( i=0; i < TLS_NR_CIPHERS; i++ )); do
+ [[ "$1" == ${TLS_CIPHER_OSSL_NAME[i]} ]] && rfcname="${TLS_CIPHER_RFC_NAME[i]}" && break
+ done
+ [[ "$rfcname" == "-" ]] && rfcname=""
+ [[ -n "$rfcname" ]] && tm_out "$rfcname"
+ return 0
+}
+
+rfc2openssl() {
+ local ossl_name
+ local -i i
+
+ for (( i=0; i < TLS_NR_CIPHERS; i++ )); do
+ [[ "$1" == ${TLS_CIPHER_RFC_NAME[i]} ]] && ossl_name="${TLS_CIPHER_OSSL_NAME[i]}" && break
+ done
+ [[ "$ossl_name" == "-" ]] && ossl_name=""
+ [[ -n "$ossl_name" ]] && tm_out "$ossl_name"
+ return 0
+}
+
+openssl2hexcode() {
+ local hexc=""
+ local -i i
+
+ if [[ $TLS_NR_CIPHERS -eq 0 ]]; then
+ if "$HAS_CIPHERSUITES"; then
+ hexc="$($OPENSSL ciphers -V -ciphersuites "$TLS13_OSSL_CIPHERS" 'ALL:COMPLEMENTOFALL:@STRENGTH' | awk '/ '"$1"' / { print $1 }')"
+ else
+ hexc="$($OPENSSL ciphers -V 'ALL:COMPLEMENTOFALL:@STRENGTH' | awk '/ '"$1"' / { print $1 }')"
+ fi
+ else
+ for (( i=0; i < TLS_NR_CIPHERS; i++ )); do
+ [[ "$1" == ${TLS_CIPHER_OSSL_NAME[i]} ]] && hexc="${TLS_CIPHER_HEXCODE[i]}" && break
+ done
+ fi
+ [[ -z "$hexc" ]] && return 1
+ tm_out "$hexc"
+ return 0
+}
+
+rfc2hexcode() {
+ local hexc=""
+ local -i i
+
+ for (( i=0; i < TLS_NR_CIPHERS; i++ )); do
+ [[ "$1" == "${TLS_CIPHER_RFC_NAME[i]}" ]] && hexc="${TLS_CIPHER_HEXCODE[i]}" && break
+ done
+ [[ -z "$hexc" ]] && return 1
+ tm_out "$hexc"
+ return 0
+}
+
+show_rfc_style(){
+ local rfcname="" hexcode
+ local -i i
+
+ hexcode="$(toupper "$1")"
+ case ${#hexcode} in
+ 3) hexcode="0x00,0x${hexcode:1:2}" ;;
+ 5) hexcode="0x${hexcode:1:2},0x${hexcode:3:2}" ;;
+ 7) hexcode="0x${hexcode:1:2},0x${hexcode:3:2},0x${hexcode:5:2}" ;;
+ *) return 1 ;;
+ esac
+ for (( i=0; i < TLS_NR_CIPHERS; i++ )); do
+ [[ "$hexcode" == ${TLS_CIPHER_HEXCODE[i]} ]] && rfcname="${TLS_CIPHER_RFC_NAME[i]}" && break
+ done
+ [[ "$rfcname" == "-" ]] && rfcname=""
+ [[ -n "$rfcname" ]] && tm_out "$rfcname"
+ return 0
+}
+
+neat_header(){
+ if [[ "$DISPLAY_CIPHERNAMES" =~ rfc ]]; then
+ out "$(printf -- "Hexcode Cipher Suite Name (IANA/RFC) KeyExch. Encryption Bits")"
+ [[ "$DISPLAY_CIPHERNAMES" != rfc-only ]] && out "$(printf -- " Cipher Suite Name (OpenSSL)")"
+ outln
+ out "$(printf -- "%s------------------------------------------------------------------------------------------")"
+ [[ "$DISPLAY_CIPHERNAMES" != rfc-only ]] && out "$(printf -- "---------------------------------------")"
+ outln
+ else
+ out "$(printf -- "Hexcode Cipher Suite Name (OpenSSL) KeyExch. Encryption Bits")"
+ [[ "$DISPLAY_CIPHERNAMES" != openssl-only ]] && out "$(printf -- " Cipher Suite Name (IANA/RFC)")"
+ outln
+ out "$(printf -- "%s--------------------------------------------------------------------------")"
+ [[ "$DISPLAY_CIPHERNAMES" != openssl-only ]] && out "$(printf -- "---------------------------------------------------")"
+ outln
+ fi
+}
+
+
+# arg1: hexcode
+# arg2: cipher in openssl notation
+# arg3: keyexchange
+# arg4: encryption (maybe included "export")
+# arg5: "true" if the cipher's "quality" should be highlighted
+# "false" if the line should be printed in light grey
+# empty if line should be returned as a string
+neat_list(){
+ local hexcode="$1"
+ local ossl_cipher="$2" tls_cipher=""
+ local kx enc strength line what_dh bits
+ local -i i len
+
+ kx="${3//Kx=/}"
+ enc="${4//Enc=/}"
+ # In two cases LibreSSL uses very long names for encryption algorithms
+ # and doesn't include the number of bits.
+ [[ "$enc" == ChaCha20-Poly1305 ]] && enc="CHACHA20(256)"
+ [[ "$enc" == GOST-28178-89-CNT ]] && enc="GOST(256)"
+
+ strength="${enc//\)/}" # retrieve (). first remove trailing ")"
+ strength="${strength#*\(}" # exfiltrate (VAL
+ enc="${enc%%\(*}"
+
+ enc="${enc//POLY1305/}" # remove POLY1305
+ enc="${enc//\//}" # remove "/"
+
+ [[ "$export" =~ export ]] && strength="$strength,exp"
+
+ [[ "$DISPLAY_CIPHERNAMES" != openssl-only ]] && tls_cipher="$(show_rfc_style "$hexcode")"
+
+ if [[ "$5" != true ]]; then
+ if [[ "$DISPLAY_CIPHERNAMES" =~ rfc ]]; then
+ line="$(printf -- " %-7s %-49s %-10s %-12s%-8s" "$hexcode" "$tls_cipher" "$kx" "$enc" "$strength")"
+ [[ "$DISPLAY_CIPHERNAMES" != rfc-only ]] && line+="$(printf -- " %-33s${SHOW_EACH_C:+ %-0s}" "$ossl_cipher")"
+ else
+ line="$(printf -- " %-7s %-33s %-10s %-12s%-8s" "$hexcode" "$ossl_cipher" "$kx" "$enc" "$strength")"
+ [[ "$DISPLAY_CIPHERNAMES" != openssl-only ]] && line+="$(printf -- " %-49s${SHOW_EACH_C:+ %-0s}" "$tls_cipher")"
+ fi
+ if [[ -z "$5" ]]; then
+ tm_out "$line"
+ else
+ pr_deemphasize "$line"
+ fi
+ return 0
+ fi
+ if [[ "$kx" =~ " " ]]; then
+ what_dh="${kx%% *}"
+ bits="${kx##* }"
+ else
+ what_dh="$kx"
+ bits=""
+ fi
+ if [[ "$COLOR" -le 2 ]]; then
+ if [[ "$DISPLAY_CIPHERNAMES" =~ rfc ]]; then
+ out "$(printf -- " %-7s %-49s " "$hexcode" "$tls_cipher")"
+ else
+ out "$(printf -- " %-7s %-33s " "$hexcode" "$ossl_cipher")"
+ fi
+ else
+ out "$(printf -- " %-7s " "$hexcode")"
+ if [[ "$DISPLAY_CIPHERNAMES" =~ rfc ]]; then
+ print_fixed_width "$tls_cipher" 49 pr_cipher_quality
+ else
+ print_fixed_width "$ossl_cipher" 33 pr_cipher_quality
+ fi
+ fi
+ out "$what_dh"
+ if [[ -n "$bits" ]]; then
+ if [[ $what_dh == DH ]] || [[ $what_dh == EDH ]]; then
+ pr_dh_quality "$bits" " $bits"
+ elif [[ $what_dh == ECDH ]]; then
+ pr_ecdh_quality "$bits" " $bits"
+ fi
+ fi
+ len=${#kx}
+ for (( i=len; i<10; i++ )); do
+ out " "
+ done
+ out "$(printf -- " %-12s%-8s " "$enc" "$strength")"
+ if [[ "$COLOR" -le 2 ]]; then
+ if [[ "$DISPLAY_CIPHERNAMES" == rfc ]]; then
+ out "$(printf -- "%-33s${SHOW_EACH_C:+ %-0s}" "$ossl_cipher")"
+ elif [[ "$DISPLAY_CIPHERNAMES" == openssl ]]; then
+ out "$(printf -- "%-49s${SHOW_EACH_C:+ %-0s}" "$tls_cipher")"
+ fi
+ else
+ if [[ "$DISPLAY_CIPHERNAMES" == rfc ]]; then
+ print_fixed_width "$ossl_cipher" 32 pr_cipher_quality
+ elif [[ "$DISPLAY_CIPHERNAMES" == openssl ]]; then
+ print_fixed_width "$tls_cipher" 48 pr_cipher_quality
+ fi
+ out "$(printf -- "${SHOW_EACH_C:+ %-0s}")"
+ fi
+}
+
+run_cipher_match(){
+ local hexc n auth export ciphers_to_test tls13_ciphers_to_test supported_sslv2_ciphers s
+ local -a hexcode normalized_hexcode ciph sslvers kx enc export2 sigalg
+ local -a ciphers_found ciphers_found2 ciph2 rfc_ciph rfc_ciph2 ossl_supported
+ local -a -i index
+ local -i nr_ciphers=0 nr_ossl_ciphers=0 nr_nonossl_ciphers=0
+ local -i num_bundles mod_check bundle_size bundle end_of_bundle
+ local dhlen has_dh_bits="$HAS_DH_BITS"
+ local cipher proto protos_to_try
+ local available
+ local -i sclient_success
+ local re='^[0-9A-Fa-f]+$'
+ local using_sockets=true
+
+ "$SSL_NATIVE" && using_sockets=false
+ "$FAST" && using_sockets=false
+ [[ $TLS_NR_CIPHERS == 0 ]] && using_sockets=false
+
+ pr_headline " Testing ciphers with "
+ if [[ $1 =~ $re ]]; then
+ pr_headline "matching number pattern \"$1\" "
+ tjolines="$tjolines matching number pattern \"$1\"\n\n"
+ else
+ pr_headline "word pattern "\"$1\"" (ignore case) "
+ tjolines="$tjolines word pattern \"$1\" (ignore case)\n\n"
+ fi
+ outln
+ if ! "$using_sockets"; then
+ [[ $TLS_NR_CIPHERS == 0 ]] && ! "$SSL_NATIVE" && ! "$FAST" && pr_warning " Cipher mapping not available, doing a fallback to openssl"
+ if ! "$HAS_DH_BITS"; then
+ [[ $TLS_NR_CIPHERS == 0 ]] && ! "$SSL_NATIVE" && ! "$FAST" && out "."
+ prln_warning " (Your $OPENSSL cannot show DH/ECDH bits)"
+ fi
+ fi
+ outln
+ neat_header
+ #for arg in $(echo $@ | sed 's/,/ /g'); do
+ for arg in ${*//, /}; do
+ if "$using_sockets" || [[ $OSSL_VER_MAJOR -lt 1 ]]; then
+ for (( i=0; i < TLS_NR_CIPHERS; i++ )); do
+ hexc="${TLS_CIPHER_HEXCODE[i]}"
+ if [[ ${#hexc} -eq 9 ]]; then
+ hexcode[nr_ciphers]="${hexc:2:2},${hexc:7:2}"
+ if [[ "${hexc:2:2}" == "00" ]]; then
+ normalized_hexcode[nr_ciphers]="x${hexc:7:2}"
+ else
+ normalized_hexcode[nr_ciphers]="x${hexc:2:2}${hexc:7:2}"
+ fi
+ else
+ hexc="$(tolower "$hexc")"
+ hexcode[nr_ciphers]="${hexc:2:2},${hexc:7:2},${hexc:12:2}"
+ normalized_hexcode[nr_ciphers]="x${hexc:2:2}${hexc:7:2}${hexc:12:2}"
+ fi
+ if [[ $arg =~ $re ]]; then
+ neat_list "${normalized_hexcode[nr_ciphers]}" "${TLS_CIPHER_OSSL_NAME[i]}" "${TLS_CIPHER_KX[i]}" "${TLS_CIPHER_ENC[i]}" | grep -qai "$arg"
+ else
+ neat_list "${normalized_hexcode[nr_ciphers]}" "${TLS_CIPHER_OSSL_NAME[i]}" "${TLS_CIPHER_KX[i]}" "${TLS_CIPHER_ENC[i]}" | grep -qwai "$arg"
+ fi
+ if [[ $? -eq 0 ]] && ( "$using_sockets" || "${TLS_CIPHER_OSSL_SUPPORTED[i]}" ); then # string matches, so we can ssl to it:
+ normalized_hexcode[nr_ciphers]="$(tolower "${normalized_hexcode[nr_ciphers]}")"
+ ciph[nr_ciphers]="${TLS_CIPHER_OSSL_NAME[i]}"
+ rfc_ciph[nr_ciphers]="${TLS_CIPHER_RFC_NAME[i]}"
+ kx[nr_ciphers]="${TLS_CIPHER_KX[i]}"
+ enc[nr_ciphers]="${TLS_CIPHER_ENC[i]}"
+ sslvers[nr_ciphers]="${TLS_CIPHER_SSLVERS[i]}"
+ export2[nr_ciphers]="${TLS_CIPHER_EXPORT[i]}"
+ ciphers_found[nr_ciphers]=false
+ sigalg[nr_ciphers]=""
+ ossl_supported[nr_ciphers]="${TLS_CIPHER_OSSL_SUPPORTED[i]}"
+ if "$using_sockets" && ! "$has_dh_bits" && \
+ ( [[ ${kx[nr_ciphers]} == "Kx=ECDH" ]] || [[ ${kx[nr_ciphers]} == "Kx=DH" ]] || [[ ${kx[nr_ciphers]} == "Kx=EDH" ]] ); then
+ ossl_supported[nr_ciphers]=false
+ fi
+ nr_ciphers+=1
+ fi
+ done
+ else
+ while read hexc n ciph[nr_ciphers] sslvers[nr_ciphers] kx[nr_ciphers] auth enc[nr_ciphers] mac export2[nr_ciphers]; do
+ hexc="$(normalize_ciphercode $hexc)"
+ # is argument a number?
+ if [[ $arg =~ $re ]]; then
+ neat_list "$hexc" "${ciph[nr_ciphers]}" "${kx[nr_ciphers]}" "${enc[nr_ciphers]}" | grep -qai "$arg"
+ else
+ neat_list "$hexc" "${ciph[nr_ciphers]}" "${kx[nr_ciphers]}" "${enc[nr_ciphers]}" | grep -qwai "$arg"
+ fi
+ if [[ $? -eq 0 ]]; then # string matches, so we can ssl to it:
+ ciphers_found[nr_ciphers]=false
+ normalized_hexcode[nr_ciphers]="$hexc"
+ sigalg[nr_ciphers]=""
+ ossl_supported[nr_ciphers]=true
+ nr_ciphers+=1
+ fi
+ done < <(actually_supported_osslciphers 'ALL:COMPLEMENTOFALL:@STRENGTH' 'ALL' "-V")
+ fi
+
+ # Test the SSLv2 ciphers, if any.
+ if "$using_sockets"; then
+ ciphers_to_test=""
+ for (( i=0; i < nr_ciphers; i++ )); do
+ if [[ "${sslvers[i]}" == SSLv2 ]]; then
+ ciphers_to_test+=", ${hexcode[i]}"
+ fi
+ done
+ if [[ -n "$ciphers_to_test" ]]; then
+ sslv2_sockets "${ciphers_to_test:2}" "true"
+ if [[ $? -eq 3 ]] && [[ "$V2_HELLO_CIPHERSPEC_LENGTH" -ne 0 ]]; then
+ supported_sslv2_ciphers="$(grep "Supported cipher: " "$TEMPDIR/$NODEIP.parse_sslv2_serverhello.txt")"
+ "$SHOW_SIGALGO" && s="$(read_sigalg_from_file "$HOSTCERT")"
+ for (( i=0 ; i<nr_ciphers; i++ )); do
+ if [[ "${sslvers[i]}" == SSLv2 ]] && [[ "$supported_sslv2_ciphers" =~ ${normalized_hexcode[i]} ]]; then
+ ciphers_found[i]=true
+ "$SHOW_SIGALGO" && sigalg[i]="$s"
+ fi
+ done
+ fi
+ fi
+ else
+ ciphers_to_test=""
+ for (( i=0; i < nr_ciphers; i++ )); do
+ if [[ "${sslvers[i]}" == SSLv2 ]]; then
+ ciphers_to_test+=":${ciph[i]}"
+ fi
+ done
+ if [[ -n "$ciphers_to_test" ]]; then
+ $OPENSSL s_client -cipher "${ciphers_to_test:1}" $STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY -ssl2 >$TMPFILE 2>$ERRFILE </dev/null
+ sclient_connect_successful $? "$TMPFILE"
+ if [[ $? -eq 0 ]]; then
+ supported_sslv2_ciphers="$(grep -A 4 "Ciphers common between both SSL endpoints:" $TMPFILE)"
+ "$SHOW_SIGALGO" && s="$(read_sigalg_from_file "$TMPFILE")"
+ for (( i=0 ; i<nr_ciphers; i++ )); do
+ if [[ "${sslvers[i]}" == SSLv2 ]] && [[ "$supported_sslv2_ciphers" =~ ${ciph[i]} ]]; then
+ ciphers_found[i]=true
+ "$SHOW_SIGALGO" && sigalg[i]="$s"
+ fi
+ done
+ fi
+ fi
+ fi
+
+ for (( i=0; i < nr_ciphers; i++ )); do
+ if "${ossl_supported[i]}" && [[ "${sslvers[i]}" != "SSLv2" ]]; then
+ ciphers_found2[nr_ossl_ciphers]=false
+ ciph2[nr_ossl_ciphers]="${ciph[i]}"
+ index[nr_ossl_ciphers]=$i
+ nr_ossl_ciphers+=1
+ fi
+ done
+ if [[ $nr_ossl_ciphers -eq 0 ]]; then
+ num_bundles=0
+ else
+ # Some servers can't handle a handshake with >= 128 ciphers. So,
+ # test cipher suites in bundles of 128 or less.
+ num_bundles=$nr_ossl_ciphers/128
+ mod_check=$nr_ossl_ciphers%128
+ [[ $mod_check -ne 0 ]] && num_bundles=$num_bundles+1
+
+ bundle_size=$nr_ossl_ciphers/$num_bundles
+ mod_check=$nr_ossl_ciphers%$num_bundles
+ [[ $mod_check -ne 0 ]] && bundle_size+=1
+ fi
+
+ if "$HAS_TLS13"; then
+ protos_to_try="-no_ssl2 -tls1_2 -tls1_1 -tls1"
+ else
+ protos_to_try="-no_ssl2 -tls1_1 -tls1"
+ fi
+ "$HAS_SSL3" && protos_to_try+=" -ssl3"
+
+ for proto in $protos_to_try; do
+ if [[ "$proto" == -tls1_1 ]]; then
+ num_bundles=1
+ bundle_size=$nr_ossl_ciphers
+ fi
+ for (( bundle=0; bundle < num_bundles; bundle++ )); do
+ end_of_bundle=$bundle*$bundle_size+$bundle_size
+ [[ $end_of_bundle -gt $nr_ossl_ciphers ]] && end_of_bundle=$nr_ossl_ciphers
+ while true; do
+ ciphers_to_test=""
+ tls13_ciphers_to_test=""
+ for (( i=bundle*bundle_size; i < end_of_bundle; i++ )); do
+ if ! "${ciphers_found2[i]}"; then
+ if [[ "${ciph2[i]}" == TLS13* ]] || [[ "${ciph2[i]}" == TLS_* ]]; then
+ tls13_ciphers_to_test+=":${ciph2[i]}"
+ else
+ ciphers_to_test+=":${ciph2[i]}"
+ fi
+ fi
+ done
+ [[ -z "$ciphers_to_test" ]] && [[ -z "$tls13_ciphers_to_test" ]] && break
+ $OPENSSL s_client $(s_client_options "$proto -cipher "\'${ciphers_to_test:1}\'" -ciphersuites "\'${tls13_ciphers_to_test:1}\'" $STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY $SNI") >$TMPFILE 2>$ERRFILE </dev/null
+ sclient_connect_successful $? "$TMPFILE" || break
+ cipher=$(get_cipher $TMPFILE)
+ [[ -z "$cipher" ]] && break
+ for (( i=bundle*bundle_size; i < end_of_bundle; i++ )); do
+ [[ "$cipher" == "${ciph2[i]}" ]] && ciphers_found2[i]=true && break
+ done
+ [[ $i -eq $end_of_bundle ]] && break
+ i=${index[i]}
+ ciphers_found[i]=true
+ if [[ "$cipher" == TLS13* ]] || [[ "$cipher" == TLS_* ]]; then
+ kx[i]="$(read_dhtype_from_file $TMPFILE)"
+ fi
+ if [[ ${kx[i]} == "Kx=ECDH" ]] || [[ ${kx[i]} == "Kx=DH" ]] || [[ ${kx[i]} == "Kx=EDH" ]]; then
+ dhlen=$(read_dhbits_from_file "$TMPFILE" quiet)
+ kx[i]="${kx[i]} $dhlen"
+ fi
+ "$SHOW_SIGALGO" && grep -q "\-\-\-\-\-BEGIN CERTIFICATE\-\-\-\-\-" $TMPFILE && \
+ sigalg[i]="$(read_sigalg_from_file "$TMPFILE")"
+ done
+ done
+ done
+
+ if "$using_sockets"; then
+ for (( i=0; i < nr_ciphers; i++ )); do
+ if ! "${ciphers_found[i]}" && [[ "${sslvers[i]}" != "SSLv2" ]]; then
+ ciphers_found2[nr_nonossl_ciphers]=false
+ hexcode2[nr_nonossl_ciphers]="${hexcode[i]}"
+ rfc_ciph2[nr_nonossl_ciphers]="${rfc_ciph[i]}"
+ index[nr_nonossl_ciphers]=$i
+ nr_nonossl_ciphers+=1
+ fi
+ done
+ fi
+
+ if [[ $nr_nonossl_ciphers -eq 0 ]]; then
+ num_bundles=0
+ else
+ # Some servers can't handle a handshake with >= 128 ciphers. So,
+ # test cipher suites in bundles of 128 or less.
+ num_bundles=$nr_nonossl_ciphers/128
+ mod_check=$nr_nonossl_ciphers%128
+ [[ $mod_check -ne 0 ]] && num_bundles=$num_bundles+1
+
+ bundle_size=$nr_nonossl_ciphers/$num_bundles
+ mod_check=$nr_nonossl_ciphers%$num_bundles
+ [[ $mod_check -ne 0 ]] && bundle_size+=1
+ fi
+
+ for proto in 04 03 02 01 00; do
+ for (( bundle=0; bundle < num_bundles; bundle++ )); do
+ end_of_bundle=$bundle*$bundle_size+$bundle_size
+ [[ $end_of_bundle -gt $nr_nonossl_ciphers ]] && end_of_bundle=$nr_nonossl_ciphers
+ while true; do
+ ciphers_to_test=""
+ for (( i=bundle*bundle_size; i < end_of_bundle; i++ )); do
+ ! "${ciphers_found2[i]}" && ciphers_to_test+=", ${hexcode2[i]}"
+ done
+ [[ -z "$ciphers_to_test" ]] && break
+ [[ "$proto" == 04 ]] && [[ ! "$ciphers_to_test" =~ ,\ 13,[0-9a-f][0-9a-f] ]] && break
+ ciphers_to_test="$(strip_inconsistent_ciphers "$proto" "$ciphers_to_test")"
+ [[ -z "$ciphers_to_test" ]] && break
+ if "$SHOW_SIGALGO"; then
+ tls_sockets "$proto" "${ciphers_to_test:2}, 00,ff" "all"
+ else
+ tls_sockets "$proto" "${ciphers_to_test:2}, 00,ff" "ephemeralkey"
+ fi
+ sclient_success=$?
+ [[ $sclient_success -ne 0 ]] && [[ $sclient_success -ne 2 ]] && break
+ cipher=$(get_cipher "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt")
+ for (( i=bundle*bundle_size; i < end_of_bundle; i++ )); do
+ [[ "$cipher" == "${rfc_ciph2[i]}" ]] && ciphers_found2[i]=true && break
+ done
+ [[ $i -eq $end_of_bundle ]] && break
+ i=${index[i]}
+ ciphers_found[i]=true
+ [[ "${kx[i]}" == "Kx=any" ]] && kx[i]="$(read_dhtype_from_file "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt")"
+ if [[ ${kx[i]} == "Kx=ECDH" ]] || [[ ${kx[i]} == "Kx=DH" ]] || [[ ${kx[i]} == "Kx=EDH" ]]; then
+ dhlen=$(read_dhbits_from_file "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt" quiet)
+ kx[i]="${kx[i]} $dhlen"
+ fi
+ "$SHOW_SIGALGO" && [[ -r "$HOSTCERT" ]] && \
+ sigalg[i]="$(read_sigalg_from_file "$HOSTCERT")"
+ done
+ done
+ done
+
+ for (( i=0; i < nr_ciphers; i++ )); do
+ "${ciphers_found[i]}" || "$SHOW_EACH_C" || continue
+ export="${export2[i]}"
+ neat_list "${normalized_hexcode[i]}" "${ciph[i]}" "${kx[i]}" "${enc[i]}" "${ciphers_found[i]}"
+ available=""
+ if "$SHOW_EACH_C"; then
+ if "${ciphers_found[i]}"; then
+ available="available"
+ pr_cyan "available"
+ else
+ available="not a/v"
+ pr_deemphasize "not a/v"
+ fi
+ fi
+ outln "${sigalg[i]}"
+ fileout "cipher_${normalized_hexcode[i]}" "INFO" "$(neat_list "${normalized_hexcode[i]}" "${ciph[i]}" "${kx[i]}" "${enc[i]}") $available"
+ done
+ "$using_sockets" && HAS_DH_BITS="$has_dh_bits"
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ done
+ outln
+
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 0 # this is a single test for a cipher
+}
+
+
+
+# test for all ciphers locally configured (w/o distinguishing whether they are good or bad)
+run_allciphers() {
+ local -i nr_ciphers_tested=0 nr_ciphers=0 nr_ossl_ciphers=0 nr_nonossl_ciphers=0 sclient_success=0
+ local n auth mac export hexc sslv2_ciphers="" s
+ local -a normalized_hexcode hexcode ciph sslvers kx enc export2 sigalg ossl_supported
+ local -i i end_of_bundle bundle bundle_size num_bundles mod_check
+ local -a ciphers_found ciphers_found2 hexcode2 ciph2 rfc_ciph2
+ local -i -a index
+ local proto protos_to_try
+ local dhlen available ciphers_to_test tls13_ciphers_to_test supported_sslv2_ciphers
+ local has_dh_bits="$HAS_DH_BITS"
+ local using_sockets=true
+
+ "$SSL_NATIVE" && using_sockets=false
+ "$FAST" && using_sockets=false
+ [[ $TLS_NR_CIPHERS == 0 ]] && using_sockets=false
+
+ # get a list of all the cipher suites to test
+ if "$using_sockets" || [[ $OSSL_VER_MAJOR -lt 1 ]]; then
+ for (( i=0; i < TLS_NR_CIPHERS; i++ )); do
+ hexc="$(tolower "${TLS_CIPHER_HEXCODE[i]}")"
+ ciph[i]="${TLS_CIPHER_OSSL_NAME[i]}"
+ sslvers[i]="${TLS_CIPHER_SSLVERS[i]}"
+ kx[i]="${TLS_CIPHER_KX[i]}"
+ enc[i]="${TLS_CIPHER_ENC[i]}"
+ export2[i]="${TLS_CIPHER_EXPORT[i]}"
+ ciphers_found[i]=false
+ sigalg[i]=""
+ ossl_supported[i]=${TLS_CIPHER_OSSL_SUPPORTED[i]}
+ if "$using_sockets" && ! "$HAS_DH_BITS" && ( [[ ${kx[i]} == Kx=ECDH ]] || [[ ${kx[i]} == Kx=DH ]] || [[ ${kx[i]} == Kx=EDH ]] ); then
+ ossl_supported[i]=false
+ fi
+ if [[ ${#hexc} -eq 9 ]]; then
+ hexcode[i]="${hexc:2:2},${hexc:7:2}"
+ if [[ "${hexc:2:2}" == 00 ]]; then
+ normalized_hexcode[i]="x${hexc:7:2}"
+ else
+ normalized_hexcode[i]="x${hexc:2:2}${hexc:7:2}"
+ fi
+ else
+ hexcode[i]="${hexc:2:2},${hexc:7:2},${hexc:12:2}"
+ normalized_hexcode[i]="x${hexc:2:2}${hexc:7:2}${hexc:12:2}"
+ sslv2_ciphers="$sslv2_ciphers, ${hexcode[i]}"
+ fi
+ if "$using_sockets" || "${TLS_CIPHER_OSSL_SUPPORTED[i]}"; then
+ nr_ciphers_tested+=1
+ fi
+ done
+ nr_ciphers=$TLS_NR_CIPHERS
+ else
+ while read -r hexc n ciph[nr_ciphers] sslvers[nr_ciphers] kx[nr_ciphers] auth enc[nr_ciphers] mac export2[nr_ciphers]; do
+ ciphers_found[nr_ciphers]=false
+ if [[ ${#hexc} -eq 9 ]]; then
+ if [[ "${hexc:2:2}" == 00 ]]; then
+ normalized_hexcode[nr_ciphers]="$(tolower "x${hexc:7:2}")"
+ else
+ normalized_hexcode[nr_ciphers]="$(tolower "x${hexc:2:2}${hexc:7:2}")"
+ fi
+ else
+ normalized_hexcode[nr_ciphers]="$(tolower "x${hexc:2:2}${hexc:7:2}${hexc:12:2}")"
+ fi
+ sigalg[nr_ciphers]=""
+ ossl_supported[nr_ciphers]=true
+ nr_ciphers=$nr_ciphers+1
+ done < <(actually_supported_osslciphers 'ALL:COMPLEMENTOFALL:@STRENGTH' 'ALL' "-V")
+ nr_ciphers_tested=$nr_ciphers
+ fi
+
+ if "$using_sockets"; then
+ sslv2_sockets "${sslv2_ciphers:2}" "true"
+ if [[ $? -eq 3 ]] && [[ "$V2_HELLO_CIPHERSPEC_LENGTH" -ne 0 ]]; then
+ supported_sslv2_ciphers="$(grep "Supported cipher: " "$TEMPDIR/$NODEIP.parse_sslv2_serverhello.txt")"
+ "$SHOW_SIGALGO" && s="$(read_sigalg_from_file "$HOSTCERT")"
+ for (( i=0 ; i<nr_ciphers; i++ )); do
+ if [[ "${sslvers[i]}" == SSLv2 ]] && [[ "$supported_sslv2_ciphers" =~ ${normalized_hexcode[i]} ]]; then
+ ciphers_found[i]=true
+ "$SHOW_SIGALGO" && sigalg[i]="$s"
+ fi
+ done
+ fi
+ elif "$HAS_SSL2"; then
+ $OPENSSL s_client $STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY -ssl2 >$TMPFILE 2>$ERRFILE </dev/null
+ sclient_connect_successful $? "$TMPFILE"
+ if [[ $? -eq 0 ]]; then
+ supported_sslv2_ciphers="$(grep -A 4 "Ciphers common between both SSL endpoints:" $TMPFILE)"
+ "$SHOW_SIGALGO" && s="$(read_sigalg_from_file "$TMPFILE")"
+ for (( i=0 ; i<nr_ciphers; i++ )); do
+ if [[ "${sslvers[i]}" == SSLv2 ]] && [[ "$supported_sslv2_ciphers" =~ ${ciph[i]} ]]; then
+ ciphers_found[i]=true
+ "$SHOW_SIGALGO" && sigalg[i]="$s"
+ fi
+ done
+ fi
+ fi
+
+ outln
+ if "$using_sockets"; then
+ pr_headlineln " Testing $nr_ciphers_tested ciphers via OpenSSL plus sockets against the server, ordered by encryption strength "
+ else
+ pr_headlineln " Testing all $nr_ciphers_tested locally available ciphers against the server, ordered by encryption strength "
+ [[ $TLS_NR_CIPHERS == 0 ]] && ! "$SSL_NATIVE" && ! "$FAST" && pr_warning " Cipher mapping not available, doing a fallback to openssl"
+ outln
+ if ! "$HAS_DH_BITS"; then
+ [[ $TLS_NR_CIPHERS == 0 ]] && ! "$SSL_NATIVE" && ! "$FAST" && out "."
+ prln_warning " Your $OPENSSL cannot show DH/ECDH bits"
+ fi
+ fi
+ outln
+ neat_header
+
+ for (( i=0; i < nr_ciphers; i++ )); do
+ if "${ossl_supported[i]}"; then
+ [[ "${sslvers[i]}" == SSLv2 ]] && continue
+ ciphers_found2[nr_ossl_ciphers]=false
+ ciph2[nr_ossl_ciphers]="${ciph[i]}"
+ index[nr_ossl_ciphers]=$i
+ nr_ossl_ciphers+=1
+ fi
+ done
+
+ if [[ $nr_ossl_ciphers -eq 0 ]]; then
+ num_bundles=0
+ else
+ # Some servers can't handle a handshake with >= 128 ciphers. So,
+ # test cipher suites in bundles of 128 or less.
+ num_bundles=$nr_ossl_ciphers/128
+ mod_check=$nr_ossl_ciphers%128
+ [[ $mod_check -ne 0 ]] && num_bundles=$num_bundles+1
+
+ bundle_size=$nr_ossl_ciphers/$num_bundles
+ mod_check=$nr_ossl_ciphers%$num_bundles
+ [[ $mod_check -ne 0 ]] && bundle_size+=1
+ fi
+
+ if "$HAS_TLS13"; then
+ protos_to_try="-no_ssl2 -tls1_2 -tls1_1 -tls1"
+ else
+ protos_to_try="-no_ssl2 -tls1_1 -tls1"
+ fi
+ "$HAS_SSL3" && protos_to_try+=" -ssl3"
+
+ for proto in $protos_to_try; do
+ if [[ "$proto" == -tls1_1 ]]; then
+ num_bundles=1
+ bundle_size=$nr_ossl_ciphers
+ fi
+
+ [[ "$proto" != "-no_ssl2" ]] && [[ $(has_server_protocol "${proto:1}") -eq 1 ]] && continue
+ for (( bundle=0; bundle < num_bundles; bundle++ )); do
+ end_of_bundle=$bundle*$bundle_size+$bundle_size
+ [[ $end_of_bundle -gt $nr_ossl_ciphers ]] && end_of_bundle=$nr_ossl_ciphers
+ while true; do
+ ciphers_to_test=""
+ tls13_ciphers_to_test=""
+ for (( i=bundle*bundle_size; i < end_of_bundle; i++ )); do
+ if ! "${ciphers_found2[i]}"; then
+ if [[ "${ciph2[i]}" == TLS13* ]] || [[ "${ciph2[i]}" == TLS_* ]]; then
+ tls13_ciphers_to_test+=":${ciph2[i]}"
+ else
+ ciphers_to_test+=":${ciph2[i]}"
+ fi
+ fi
+ done
+ [[ -z "$ciphers_to_test" ]] && [[ -z "$tls13_ciphers_to_test" ]] && break
+ $OPENSSL s_client $(s_client_options "$proto -cipher "\'${ciphers_to_test:1}\'" -ciphersuites "\'${tls13_ciphers_to_test:1}\'" $STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY $SNI") >$TMPFILE 2>$ERRFILE </dev/null
+ sclient_connect_successful $? "$TMPFILE" || break
+ cipher=$(get_cipher $TMPFILE)
+ [[ -z "$cipher" ]] && break
+ for (( i=bundle*bundle_size; i < end_of_bundle; i++ )); do
+ [[ "$cipher" == "${ciph2[i]}" ]] && ciphers_found2[i]=true && break
+ done
+ [[ $i -eq $end_of_bundle ]] && break
+ i=${index[i]}
+ ciphers_found[i]=true
+ if [[ "$cipher" == TLS13* ]] || [[ "$cipher" == TLS_* ]]; then
+ kx[i]="$(read_dhtype_from_file $TMPFILE)"
+ fi
+ if [[ ${kx[i]} == Kx=ECDH ]] || [[ ${kx[i]} == Kx=DH ]] || [[ ${kx[i]} == Kx=EDH ]]; then
+ dhlen=$(read_dhbits_from_file "$TMPFILE" quiet)
+ kx[i]="${kx[i]} $dhlen"
+ fi
+ "$SHOW_SIGALGO" && grep -q "\-\-\-\-\-BEGIN CERTIFICATE\-\-\-\-\-" $TMPFILE && \
+ sigalg[i]="$(read_sigalg_from_file "$TMPFILE")"
+ done
+ done
+ done
+
+ if "$using_sockets"; then
+ for (( i=0; i < nr_ciphers; i++ )); do
+ if ! "${ciphers_found[i]}"; then
+ [[ "${sslvers[i]}" == SSLv2 ]] && continue
+ ciphers_found2[nr_nonossl_ciphers]=false
+ hexcode2[nr_nonossl_ciphers]="${hexcode[i]}"
+ rfc_ciph2[nr_nonossl_ciphers]="${TLS_CIPHER_RFC_NAME[i]}"
+ index[nr_nonossl_ciphers]=$i
+ nr_nonossl_ciphers+=1
+ fi
+ done
+ fi
+
+ if [[ $nr_nonossl_ciphers -eq 0 ]]; then
+ num_bundles=0
+ else
+ # Some servers can't handle a handshake with >= 128 ciphers. So,
+ # test cipher suites in bundles of 128 or less.
+ num_bundles=$nr_nonossl_ciphers/128
+ mod_check=$nr_nonossl_ciphers%128
+ [[ $mod_check -ne 0 ]] && num_bundles=$num_bundles+1
+
+ bundle_size=$nr_nonossl_ciphers/$num_bundles
+ mod_check=$nr_nonossl_ciphers%$num_bundles
+ [[ $mod_check -ne 0 ]] && bundle_size+=1
+ fi
+
+ for proto in 04 03 02 01 00; do
+ for (( bundle=0; bundle < num_bundles; bundle++ )); do
+ end_of_bundle=$bundle*$bundle_size+$bundle_size
+ [[ $end_of_bundle -gt $nr_nonossl_ciphers ]] && end_of_bundle=$nr_nonossl_ciphers
+ while true; do
+ ciphers_to_test=""
+ for (( i=bundle*bundle_size; i < end_of_bundle; i++ )); do
+ ! "${ciphers_found2[i]}" && ciphers_to_test+=", ${hexcode2[i]}"
+ done
+ [[ -z "$ciphers_to_test" ]] && break
+ [[ "$proto" == 04 ]] && [[ ! "$ciphers_to_test" =~ ,\ 13,[0-9a-f][0-9a-f] ]] && break
+ ciphers_to_test="$(strip_inconsistent_ciphers "$proto" "$ciphers_to_test")"
+ [[ -z "$ciphers_to_test" ]] && break
+ if "$SHOW_SIGALGO"; then
+ tls_sockets "$proto" "${ciphers_to_test:2}, 00,ff" "all"
+ else
+ tls_sockets "$proto" "${ciphers_to_test:2}, 00,ff" "ephemeralkey"
+ fi
+ sclient_success=$?
+ [[ $sclient_success -ne 0 ]] && [[ $sclient_success -ne 2 ]] && break
+ cipher=$(get_cipher "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt")
+ for (( i=bundle*bundle_size; i < end_of_bundle; i++ )); do
+ [[ "$cipher" == "${rfc_ciph2[i]}" ]] && ciphers_found2[i]=true && break
+ done
+ [[ $i -eq $end_of_bundle ]] && break
+ i=${index[i]}
+ ciphers_found[i]=true
+ [[ "${kx[i]}" == "Kx=any" ]] && kx[i]="$(read_dhtype_from_file "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt")"
+ if [[ ${kx[i]} == "Kx=ECDH" ]] || [[ ${kx[i]} == "Kx=DH" ]] || [[ ${kx[i]} == "Kx=EDH" ]]; then
+ dhlen=$(read_dhbits_from_file "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt" quiet)
+ kx[i]="${kx[i]} $dhlen"
+ fi
+ "$SHOW_SIGALGO" && [[ -r "$HOSTCERT" ]] && sigalg[i]="$(read_sigalg_from_file "$HOSTCERT")"
+ done
+ done
+ done
+
+ for (( i=0 ; i<nr_ciphers; i++ )); do
+ if "${ciphers_found[i]}" || ( "$SHOW_EACH_C" && ( "$using_sockets" || "${ossl_supported[i]}" ) ); then
+ export=${export2[i]}
+ neat_list "${normalized_hexcode[i]}" "${ciph[i]}" "${kx[i]}" "${enc[i]}" "${ciphers_found[i]}"
+ available=""
+ if "$SHOW_EACH_C"; then
+ if ${ciphers_found[i]}; then
+ available="available"
+ pr_cyan "$available"
+ else
+ available="not a/v"
+ pr_deemphasize "$available"
+ fi
+ fi
+ outln "${sigalg[i]}"
+ fileout "cipher_${normalized_hexcode[i]}" "INFO" "$(neat_list "${normalized_hexcode[i]}" "${ciph[i]}" "${kx[i]}" "${enc[i]}") $available"
+ fi
+ done
+ "$using_sockets" && HAS_DH_BITS="$has_dh_bits"
+
+ outln
+ [[ $sclient_success -ge 6 ]] && return 1
+ return 0
+}
+
+# test for all ciphers per protocol locally configured (w/o distinguishing whether they are good or bad)
+# for the specified protocol, test for all ciphers locally configured (w/o distinguishing whether they
+# are good or bad) and list them in order to encryption strength.
+ciphers_by_strength() {
+ local proto="$1" proto_hex="$2" proto_text="$3"
+ local using_sockets="$4"
+ local ossl_ciphers_proto
+ local -i nr_ciphers nr_ossl_ciphers nr_nonossl_ciphers success
+ local n sslvers auth mac export hexc sslv2_ciphers="" cipher
+ local -a hexcode normalized_hexcode ciph rfc_ciph kx enc export2
+ local -a hexcode2 ciph2 rfc_ciph2
+ local -i i bundle end_of_bundle bundle_size num_bundles mod_check
+ local -a ciphers_found ciphers_found2 sigalg ossl_supported index
+ local dhlen supported_sslv2_ciphers ciphers_to_test tls13_ciphers_to_test addcmd temp
+ local available
+ local id
+ local has_dh_bits="$HAS_DH_BITS"
+
+ pr_underline "$(printf -- "%b" "$proto_text")"
+ # for local problem if it happens
+ out " "
+ if ! "$using_sockets" && ! locally_supported "$proto"; then
+ return 0
+ fi
+ outln
+
+ [[ $(has_server_protocol "${proto:1}") -eq 1 ]] && return 0
+
+ # get a list of all the cipher suites to test
+ nr_ciphers=0
+ if "$using_sockets" || [[ $OSSL_VER_MAJOR -lt 1 ]]; then
+ for (( i=0; i < TLS_NR_CIPHERS; i++ )); do
+ hexc="${TLS_CIPHER_HEXCODE[i]}"
+ ciph[nr_ciphers]="${TLS_CIPHER_OSSL_NAME[i]}"
+ rfc_ciph[nr_ciphers]="${TLS_CIPHER_RFC_NAME[i]}"
+ kx[nr_ciphers]="${TLS_CIPHER_KX[i]}"
+ enc[nr_ciphers]="${TLS_CIPHER_ENC[i]}"
+ export2[nr_ciphers]="${TLS_CIPHER_EXPORT[i]}"
+ ciphers_found[nr_ciphers]=false
+ sigalg[nr_ciphers]=""
+ ossl_supported[nr_ciphers]=${TLS_CIPHER_OSSL_SUPPORTED[i]}
+ if "$using_sockets" && ! "$has_dh_bits" && ( [[ ${kx[nr_ciphers]} == "Kx=ECDH" ]] || [[ ${kx[nr_ciphers]} == "Kx=DH" ]] || [[ ${kx[nr_ciphers]} == "Kx=EDH" ]] ); then
+ ossl_supported[nr_ciphers]=false
+ fi
+ if [[ ${#hexc} -eq 9 ]]; then
+ hexcode[nr_ciphers]="${hexc:2:2},${hexc:7:2}"
+ if [[ "${hexc:2:2}" == 00 ]]; then
+ normalized_hexcode[nr_ciphers]="x${hexc:7:2}"
+ else
+ normalized_hexcode[nr_ciphers]="x${hexc:2:2}${hexc:7:2}"
+ fi
+ else
+ hexc="$(tolower "$hexc")"
+ hexcode[nr_ciphers]="${hexc:2:2},${hexc:7:2},${hexc:12:2}"
+ normalized_hexcode[nr_ciphers]="x${hexc:2:2}${hexc:7:2}${hexc:12:2}"
+ fi
+ if ( "$using_sockets" || "${TLS_CIPHER_OSSL_SUPPORTED[i]}" ); then
+ if [[ ${#hexc} -eq 9 ]] && [[ "$proto_text" != SSLv2 ]]; then
+ if [[ "$proto_text" == TLS\ 1.3 ]]; then
+ [[ "${hexc:2:2}" == 13 ]] && nr_ciphers+=1
+ elif [[ "$proto_text" == TLS\ 1.2 ]]; then
+ [[ "${hexc:2:2}" != 13 ]] && nr_ciphers+=1
+ elif [[ ! "${TLS_CIPHER_RFC_NAME[i]}" =~ SHA256 ]] && [[ ! "${TLS_CIPHER_RFC_NAME[i]}" =~ SHA384 ]] && \
+ [[ "${TLS_CIPHER_RFC_NAME[i]}" != *_CCM ]] && [[ "${TLS_CIPHER_RFC_NAME[i]}" != *_CCM_8 ]]; then
+ nr_ciphers+=1
+ fi
+ elif [[ ${#hexc} -eq 14 ]] && [[ "$proto_text" == SSLv2 ]]; then
+ sslv2_ciphers+=", ${hexcode[nr_ciphers]}"
+ nr_ciphers+=1
+ fi
+ fi
+ done
+ else # no sockets, openssl!
+ # The OpenSSL ciphers function, prior to version 1.1.0, could only understand -ssl2, -ssl3, and -tls1.
+ if [[ "$OSSL_NAME" =~ LibreSSL ]]; then
+ ossl_ciphers_proto=""
+ elif [[ "$proto" == -ssl2 ]] || [[ "$proto" == -ssl3 ]] || \
+ [[ $OSSL_VER_MAJOR.$OSSL_VER_MINOR == 1.1.0* ]] || [[ $OSSL_VER_MAJOR.$OSSL_VER_MINOR == 1.1.1* ]] || \
+ [[ $OSSL_VER_MAJOR.$OSSL_VER_MINOR == 3.0.0* ]]; then
+ ossl_ciphers_proto="$proto"
+ else
+ ossl_ciphers_proto="-tls1"
+ fi
+ while read hexc n ciph[nr_ciphers] sslvers kx[nr_ciphers] auth enc[nr_ciphers] mac export2[nr_ciphers]; do
+ if [[ "$proto_text" == TLS\ 1.3 ]]; then
+ [[ "${ciph[nr_ciphers]}" == TLS13* ]] || [[ "${ciph[nr_ciphers]}" == TLS_* ]] || continue
+ elif [[ "$proto_text" == "TLS 1.2" ]]; then
+ if [[ "${ciph[nr_ciphers]}" == TLS13* ]] || [[ "${ciph[nr_ciphers]}" == TLS_* ]]; then
+ continue
+ fi
+ elif [[ "${ciph[nr_ciphers]}" == *-SHA256 ]] || [[ "${ciph[nr_ciphers]}" == *-SHA384 ]] || \
+ [[ "${ciph[nr_ciphers]}" == *-CCM ]] || [[ "${ciph[nr_ciphers]}" == *-CCM8 ]] || \
+ [[ "${ciph[nr_ciphers]}" =~ CHACHA20-POLY1305 ]]; then
+ continue
+ fi
+ ciphers_found[nr_ciphers]=false
+ normalized_hexcode[nr_ciphers]="$(normalize_ciphercode "$hexc")"
+ sigalg[nr_ciphers]=""
+ ossl_supported[nr_ciphers]=true
+ nr_ciphers+=1
+ done < <(actually_supported_osslciphers 'ALL:COMPLEMENTOFALL:@STRENGTH' 'ALL' "$ossl_ciphers_proto -V")
+ fi
+
+ if [[ "$proto" == -ssl2 ]]; then
+ if "$using_sockets"; then
+ sslv2_sockets "${sslv2_ciphers:2}" "true"
+ if [[ $? -eq 3 ]] && [[ "$V2_HELLO_CIPHERSPEC_LENGTH" -ne 0 ]]; then
+ supported_sslv2_ciphers="$(grep "Supported cipher: " "$TEMPDIR/$NODEIP.parse_sslv2_serverhello.txt")"
+ "$SHOW_SIGALGO" && s="$(read_sigalg_from_file "$HOSTCERT")"
+ for (( i=0 ; i<nr_ciphers; i++ )); do
+ if [[ "$supported_sslv2_ciphers" =~ ${normalized_hexcode[i]} ]]; then
+ ciphers_found[i]=true
+ "$SHOW_SIGALGO" && sigalg[i]="$s"
+ fi
+ done
+ fi
+ else
+ $OPENSSL s_client $STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY -ssl2 >$TMPFILE 2>$ERRFILE </dev/null
+ sclient_connect_successful $? "$TMPFILE"
+ if [[ $? -eq 0 ]]; then
+ supported_sslv2_ciphers="$(grep -A 4 "Ciphers common between both SSL endpoints:" $TMPFILE)"
+ "$SHOW_SIGALGO" && s="$(read_sigalg_from_file "$TMPFILE")"
+ for (( i=0 ; i<nr_ciphers; i++ )); do
+ if [[ "$supported_sslv2_ciphers" =~ ${ciph[i]} ]]; then
+ ciphers_found[i]=true
+ "$SHOW_SIGALGO" && sigalg[i]="$s"
+ fi
+ done
+ fi
+ fi
+ else # no SSLv2
+ nr_ossl_ciphers=0
+ if ( "$HAS_SSL3" || [[ $proto != -ssl3 ]] ) && ( "$HAS_TLS13" || [[ $proto != -tls1_3 ]] ); then
+ for (( i=0; i < nr_ciphers; i++ )); do
+ if "${ossl_supported[i]}"; then
+ ciphers_found2[nr_ossl_ciphers]=false
+ ciph2[nr_ossl_ciphers]="${ciph[i]}"
+ index[nr_ossl_ciphers]=$i
+ nr_ossl_ciphers+=1
+ fi
+ done
+ fi
+ if [[ $nr_ossl_ciphers -eq 0 ]]; then
+ num_bundles=0
+ else
+ # Some servers can't handle a handshake with >= 128 ciphers. So,
+ # test cipher suites in bundles of 128 or less.
+ num_bundles=$nr_ossl_ciphers/128
+ mod_check=$nr_ossl_ciphers%128
+ [[ $mod_check -ne 0 ]] && num_bundles=$num_bundles+1
+
+ bundle_size=$nr_ossl_ciphers/$num_bundles
+ mod_check=$nr_ossl_ciphers%$num_bundles
+ [[ $mod_check -ne 0 ]] && bundle_size+=1
+ fi
+
+ for (( bundle=0; bundle < num_bundles; bundle++ )); do
+ end_of_bundle=$bundle*$bundle_size+$bundle_size
+ [[ $end_of_bundle -gt $nr_ossl_ciphers ]] && end_of_bundle=$nr_ossl_ciphers
+ for (( success=0; success==0 ; 1 )); do
+ ciphers_to_test=""
+ tls13_ciphers_to_test=""
+ for (( i=bundle*bundle_size; i < end_of_bundle; i++ )); do
+ if ! "${ciphers_found2[i]}"; then
+ if [[ "$proto" == -tls1_3 ]]; then
+ tls13_ciphers_to_test+=":${ciph2[i]}"
+ else
+ ciphers_to_test+=":${ciph2[i]}"
+ fi
+ fi
+ done
+ success=1
+ if [[ -n "$ciphers_to_test" ]] || [[ -n "$tls13_ciphers_to_test" ]]; then
+ $OPENSSL s_client $(s_client_options "-cipher "\'${ciphers_to_test:1}\'" -ciphersuites "\'${tls13_ciphers_to_test:1}\'" $proto $STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY $SNI") >$TMPFILE 2>$ERRFILE </dev/null
+ sclient_connect_successful $? "$TMPFILE"
+ if [[ $? -eq 0 ]]; then
+ cipher=$(get_cipher $TMPFILE)
+ if [[ -n "$cipher" ]]; then
+ success=0
+ for (( i=bundle*bundle_size; i < end_of_bundle; i++ )); do
+ [[ "$cipher" == "${ciph2[i]}" ]] && ciphers_found2[i]=true && break
+ done
+ i=${index[i]}
+ ciphers_found[i]=true
+ [[ "$proto_text" == TLS\ 1.3 ]] && kx[i]="$(read_dhtype_from_file $TMPFILE)"
+ if [[ ${kx[i]} == Kx=ECDH ]] || [[ ${kx[i]} == Kx=DH ]] || [[ ${kx[i]} == Kx=EDH ]]; then
+ dhlen=$(read_dhbits_from_file "$TMPFILE" quiet)
+ kx[i]="${kx[i]} $dhlen"
+ fi
+ "$SHOW_SIGALGO" && grep -q "\-\-\-\-\-BEGIN CERTIFICATE\-\-\-\-\-" $TMPFILE && \
+ sigalg[i]="$(read_sigalg_from_file "$TMPFILE")"
+ fi
+ fi
+ fi
+ done
+ done
+
+ if "$using_sockets"; then
+ nr_nonossl_ciphers=0
+ for (( i=0; i < nr_ciphers; i++ )); do
+ if ! "${ciphers_found[i]}"; then
+ ciphers_found2[nr_nonossl_ciphers]=false
+ hexcode2[nr_nonossl_ciphers]="${hexcode[i]}"
+ rfc_ciph2[nr_nonossl_ciphers]="${rfc_ciph[i]}"
+ index[nr_nonossl_ciphers]=$i
+ nr_nonossl_ciphers+=1
+ fi
+ done
+ fi
+
+ if [[ $nr_nonossl_ciphers -eq 0 ]]; then
+ num_bundles=0
+ else
+ # Some servers can't handle a handshake with >= 128 ciphers. So,
+ # test cipher suites in bundles of 128 or less.
+ num_bundles=$nr_nonossl_ciphers/128
+ mod_check=$nr_nonossl_ciphers%128
+ [[ $mod_check -ne 0 ]] && num_bundles=$num_bundles+1
+
+ bundle_size=$nr_nonossl_ciphers/$num_bundles
+ mod_check=$nr_nonossl_ciphers%$num_bundles
+ [[ $mod_check -ne 0 ]] && bundle_size+=1
+ fi
+
+ for (( bundle=0; bundle < num_bundles; bundle++ )); do
+ end_of_bundle=$bundle*$bundle_size+$bundle_size
+ [[ $end_of_bundle -gt $nr_nonossl_ciphers ]] && end_of_bundle=$nr_nonossl_ciphers
+ for (( success=0; success==0 ; 1 )); do
+ ciphers_to_test=""
+ for (( i=bundle*bundle_size; i < end_of_bundle; i++ )); do
+ ! "${ciphers_found2[i]}" && ciphers_to_test+=", ${hexcode2[i]}"
+ done
+ success=1
+ if [[ -n "$ciphers_to_test" ]]; then
+ if "$SHOW_SIGALGO"; then
+ tls_sockets "$proto_hex" "${ciphers_to_test:2}, 00,ff" "all"
+ else
+ tls_sockets "$proto_hex" "${ciphers_to_test:2}, 00,ff" "ephemeralkey"
+ fi
+ if [[ $? -eq 0 ]]; then
+ success=0
+ cipher=$(get_cipher "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt")
+ for (( i=bundle*bundle_size; i < end_of_bundle; i++ )); do
+ [[ "$cipher" == "${rfc_ciph2[i]}" ]] && ciphers_found2[i]=true && break
+ done
+ i=${index[i]}
+ ciphers_found[i]=true
+ [[ "$proto_text" == TLS\ 1.3 ]] && kx[i]="$(read_dhtype_from_file "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt")"
+ if [[ ${kx[i]} == Kx=ECDH ]] || [[ ${kx[i]} == Kx=DH ]] || [[ ${kx[i]} == Kx=EDH ]]; then
+ dhlen=$(read_dhbits_from_file "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt" quiet)
+ kx[i]="${kx[i]} $dhlen"
+ fi
+ "$SHOW_SIGALGO" && [[ -r "$HOSTCERT" ]] && \
+ sigalg[i]="$(read_sigalg_from_file "$HOSTCERT")"
+ fi
+ fi
+ done
+ done
+ fi
+
+ for (( i=0 ; i<nr_ciphers; i++ )); do
+ if "${ciphers_found[i]}" || "$SHOW_EACH_C"; then
+ export=${export2[i]}
+ normalized_hexcode[i]="$(tolower "${normalized_hexcode[i]}")"
+ neat_list "${normalized_hexcode[i]}" "${ciph[i]}" "${kx[i]}" "${enc[i]}" "${ciphers_found[i]}"
+ available=""
+ if "$SHOW_EACH_C"; then
+ if "${ciphers_found[i]}"; then
+ available="available"
+ pr_cyan "$available"
+ else
+ available="not a/v"
+ pr_deemphasize "$available"
+ fi
+ fi
+ outln "${sigalg[i]}"
+ id="cipher$proto"
+ id+="_${normalized_hexcode[i]}"
+ fileout "$id" "INFO" "$proto_text $(neat_list "${normalized_hexcode[i]}" "${ciph[i]}" "${kx[i]}" "${enc[i]}") $available"
+ fi
+ done
+
+ "$using_sockets" && HAS_DH_BITS="$has_dh_bits"
+ tmpfile_handle ${FUNCNAME[0]}${proto}.txt
+ return 0
+#FIXME: no error condition
+}
+
+# test for all ciphers per protocol locally configured (w/o distinguishing whether they are good or bad)
+run_cipher_per_proto() {
+ local proto proto_hex proto_text
+ local using_sockets=true
+
+ "$SSL_NATIVE" && using_sockets=false
+ "$FAST" && using_sockets=false
+ [[ $TLS_NR_CIPHERS == 0 ]] && using_sockets=false
+
+ outln
+ if "$using_sockets"; then
+ pr_headlineln " Testing ciphers per protocol via OpenSSL plus sockets against the server, ordered by encryption strength "
+ else
+ pr_headlineln " Testing all locally available ciphers per protocol against the server, ordered by encryption strength "
+ [[ $TLS_NR_CIPHERS == 0 ]] && ! "$SSL_NATIVE" && ! "$FAST" && pr_warning " Cipher mapping not available, doing a fallback to openssl"
+ outln
+ if ! "$HAS_DH_BITS"; then
+ [[ $TLS_NR_CIPHERS == 0 ]] && ! "$SSL_NATIVE" && ! "$FAST" && out "."
+ prln_warning " (Your $OPENSSL cannot show DH/ECDH bits)"
+ fi
+ fi
+ outln
+ neat_header
+ echo -e " -ssl2 22 SSLv2\n -ssl3 00 SSLv3\n -tls1 01 TLS 1\n -tls1_1 02 TLS 1.1\n -tls1_2 03 TLS 1.2\n -tls1_3 04 TLS 1.3" | while read proto proto_hex proto_text; do
+ ciphers_by_strength "$proto" "$proto_hex" "$proto_text" "$using_sockets"
+ done
+ return 0
+#FIXME: no error condition
+}
+
+# arg1 is an ASCII-HEX encoded SSLv3 or TLS ClientHello.
+# arg2: new key_share extension (only present to response to HelloRetryRequest)
+# arg3: cookie extension (if needed for response to HelloRetryRequest)
+#
+# This function may be used to either modify a ClientHello for client simulation
+# or to create a second ClientHello in response to a HelloRetryRequest.
+# If arg2 is present, then this is a response to a HelloRetryRequest, so the
+# function replaces the key_share extension with arg2 and adds the cookie
+# extension, if present.
+# If arg2 is not present, then this is an initial ClientHello for client simulation.
+# In this case, if the provided ClientHello contains a server name extension,
+# then either:
+# 1) replace it with one corresponding to $SNI; or
+# 2) remove it, if $SNI is empty
+modify_clienthello() {
+ local tls_handshake_ascii="$1"
+ local new_key_share="$2" cookie="$3"
+ local -i len offset tls_handshake_ascii_len len_all len_clienthello
+ local -i len_extensions len_extension
+ local tls_content_type tls_version_reclayer handshake_msg_type tls_clientversion
+ local tls_random tls_sid tls_cipher_suites tls_compression_methods
+ local tls_extensions="" extension_type len_extensions_hex
+ local len_servername hexdump_format_str servername_hexstr
+ local len_servername_hex len_sni_listlen len_sni_ext
+ local tls_client_hello len_clienthello_hex tls_handshake_ascii_len_hex
+ local sni_extension_found=false
+
+ tls_handshake_ascii_len=${#tls_handshake_ascii}
+
+ tls_content_type="${tls_handshake_ascii:0:2}"
+ tls_version_reclayer="${tls_handshake_ascii:2:4}"
+ len_all=$(hex2dec "${tls_handshake_ascii:6:4}")
+
+ handshake_msg_type="${tls_handshake_ascii:10:2}"
+ len_clienthello=$(hex2dec "${tls_handshake_ascii:12:6}")
+ tls_clientversion="${tls_handshake_ascii:18:4}"
+ tls_random="${tls_handshake_ascii:22:64}"
+ len=2*$(hex2dec "${tls_handshake_ascii:86:2}")+2
+ tls_sid="${tls_handshake_ascii:86:$len}"
+ offset=86+$len
+
+ len=2*$(hex2dec "${tls_handshake_ascii:$offset:4}")+4
+ tls_cipher_suites="${tls_handshake_ascii:$offset:$len}"
+ offset=$offset+$len
+
+ len=2*$(hex2dec "${tls_handshake_ascii:$offset:2}")+2
+ tls_compression_methods="${tls_handshake_ascii:$offset:$len}"
+ offset=$offset+$len
+
+ if [[ $offset -ge $tls_handshake_ascii_len ]]; then
+ # No extensions
+ tm_out "$tls_handshake_ascii"
+ return 0
+ fi
+
+ len_extensions=2*$(hex2dec "${tls_handshake_ascii:$offset:4}")
+ offset+=4
+ for (( 1; offset < tls_handshake_ascii_len; 1 )); do
+ extension_type="${tls_handshake_ascii:$offset:4}"
+ offset+=4
+ len_extension=2*$(hex2dec "${tls_handshake_ascii:$offset:4}")
+
+ if [[ "$extension_type" == 0000 ]] && [[ -z "$new_key_share" ]]; then
+ # If this is an initial ClientHello, then either remove
+ # the SNI extension or replace it with the correct server name.
+ sni_extension_found=true
+ if [[ -n "$SNI" ]]; then
+ # Create a server name extension that corresponds to $SNI
+ len_servername=${#NODE}
+ hexdump_format_str="$len_servername/1 \"%02x\""
+ servername_hexstr=$(printf $NODE | hexdump -v -e "${hexdump_format_str}")
+ # convert lengths we need to fill in from dec to hex:
+ len_servername_hex=$(printf "%02x\n" $len_servername)
+ len_sni_listlen=$(printf "%02x\n" $((len_servername+3)))
+ len_sni_ext=$(printf "%02x\n" $((len_servername+5)))
+ tls_extensions+="000000${len_sni_ext}00${len_sni_listlen}0000${len_servername_hex}${servername_hexstr}"
+ fi
+ offset+=$len_extension+4
+ elif [[ "$extension_type" != 00$KEY_SHARE_EXTN_NR ]] || [[ -z "$new_key_share" ]]; then
+ # If this is in response to a HelloRetryRequest, then do
+ # not copy over the old key_share extension, but
+ # all other extensions should be copied into the new ClientHello.
+ offset=$offset-4
+ len=$len_extension+8
+ tls_extensions+="${tls_handshake_ascii:$offset:$len}"
+ offset+=$len
+ else
+ # This is the key_share extension, and the modified ClientHello
+ # is being created in response to a HelloRetryRequest. Replace
+ # the existing key_share extension with the new one.
+ tls_extensions+="$new_key_share"
+ offset+=$len_extension+4
+ fi
+ done
+ tls_extensions+="$cookie"
+
+ if ! "$sni_extension_found" && [[ -z "$new_key_share" ]]; then
+ tm_out "$tls_handshake_ascii"
+ return 0
+ fi
+
+ len_extensions=${#tls_extensions}/2
+ len_extensions_hex=$(printf "%02x\n" $len_extensions)
+ len2twobytes "$len_extensions_hex"
+ tls_extensions="${LEN_STR:0:2}${LEN_STR:4:2}${tls_extensions}"
+
+ tls_client_hello="${tls_clientversion}${tls_random}${tls_sid}${tls_cipher_suites}${tls_compression_methods}${tls_extensions}"
+ len_clienthello=${#tls_client_hello}/2
+ len_clienthello_hex=$(printf "%02x\n" $len_clienthello)
+ len2twobytes "$len_clienthello_hex"
+ tls_handshake_ascii="${handshake_msg_type}00${LEN_STR:0:2}${LEN_STR:4:2}${tls_client_hello}"
+
+ tls_handshake_ascii_len=${#tls_handshake_ascii}/2
+ tls_handshake_ascii_len_hex=$(printf "%02x\n" $tls_handshake_ascii_len)
+ len2twobytes "$tls_handshake_ascii_len_hex"
+ tls_handshake_ascii="${tls_content_type}${tls_version_reclayer}${LEN_STR:0:2}${LEN_STR:4:2}${tls_handshake_ascii}"
+ tm_out "$tls_handshake_ascii"
+ return 0
+}
+
+client_simulation_sockets() {
+ local -i len i ret=0
+ local -i save=0
+ local lines clienthello data=""
+ local cipher_list_2send=""
+ local sock_reply_file2 sock_reply_file3
+ local tls_hello_ascii next_packet hello_done=0
+ local -i sid_len offset1 offset2
+
+ if [[ "${1:0:4}" == 1603 ]]; then
+ clienthello="$(modify_clienthello "$1")"
+ TLS_CLIENT_HELLO="${clienthello:10}"
+ else
+ clienthello="$1"
+ TLS_CLIENT_HELLO=""
+ fi
+ len=${#clienthello}
+ for (( i=0; i < len; i=i+2 )); do
+ data+=", ${clienthello:i:2}"
+ done
+ # same as above. If a CIPHER_SUITES string was provided, then check that it is in the ServerHello
+ # this appeared 1st in yassl + MySQL (https://github.com/drwetter/testssl.sh/pull/784) but adds
+ # robustness to the implementation
+ # see also https://github.com/drwetter/testssl.sh/pull/797
+ if [[ "${1:0:4}" == 1603 ]]; then
+ # Extract list of cipher suites from SSLv3 or later ClientHello
+ sid_len=4*$(hex2dec "${data:174:2}")
+ offset1=178+$sid_len
+ offset2=182+$sid_len
+ len=4*$(hex2dec "${data:offset1:2}${data:offset2:2}")-2
+ offset1=186+$sid_len
+ code2network "$(tolower "${data:offset1:len}")" # convert CIPHER_SUITES to a "standardized" format
+ else
+ # Extract list of cipher suites from SSLv2 ClientHello
+ len=2*$(hex2dec "${clienthello:12:2}")
+ for (( i=22; i < 22+len; i=i+6 )); do
+ offset1=$i+2
+ offset2=$i+4
+ [[ "${clienthello:i:2}" == 00 ]] && cipher_list_2send+=", ${clienthello:offset1:2},${clienthello:offset2:2}"
+ done
+ code2network "$(tolower "${cipher_list_2send:2}")" # convert CIPHER_SUITES to a "standardized" format
+ fi
+ cipher_list_2send="$NW_STR"
+
+ fd_socket 5 || return 6
+ debugme echo -e "\nsending client hello... "
+ socksend_clienthello "${data}"
+ sleep $USLEEP_SND
+
+ sockread_serverhello 32768
+ tls_hello_ascii=$(hexdump -v -e '16/1 "%02X"' "$SOCK_REPLY_FILE")
+ tls_hello_ascii="${tls_hello_ascii%%[!0-9A-F]*}"
+
+ # Check if the response is a HelloRetryRequest.
+ resend_if_hello_retry_request "$clienthello" "$tls_hello_ascii"
+ ret=$?
+ if [[ $ret -eq 2 ]]; then
+ tls_hello_ascii=$(hexdump -v -e '16/1 "%02X"' "$SOCK_REPLY_FILE")
+ tls_hello_ascii="${tls_hello_ascii%%[!0-9A-F]*}"
+ elif [[ $ret -eq 1 ]] || [[ $ret -eq 6 ]]; then
+ close_socket
+ TMPFILE=$SOCK_REPLY_FILE
+ tmpfile_handle ${FUNCNAME[0]}.dd
+ return $ret
+ fi
+
+ if [[ "${tls_hello_ascii:0:1}" != "8" ]]; then
+ check_tls_serverhellodone "$tls_hello_ascii" "ephemeralkey"
+ hello_done=$?
+ fi
+
+ for(( 1 ; hello_done==1; 1 )); do
+ if [[ $DEBUG -ge 1 ]]; then
+ sock_reply_file2=${SOCK_REPLY_FILE}.2
+ mv "$SOCK_REPLY_FILE" "$sock_reply_file2"
+ fi
+
+ debugme echo -n "requesting more server hello data... "
+ socksend "" $USLEEP_SND
+ sockread_serverhello 32768
+
+ next_packet=$(hexdump -v -e '16/1 "%02X"' "$SOCK_REPLY_FILE")
+ next_packet="${next_packet%%[!0-9A-F]*}"
+ if [[ ${#next_packet} -eq 0 ]]; then
+ # This shouldn't be necessary. However, it protects against
+ # getting into an infinite loop if the server has nothing
+ # left to send and check_tls_serverhellodone doesn't
+ # correctly catch it.
+ [[ $DEBUG -ge 1 ]] && mv "$sock_reply_file2" "$SOCK_REPLY_FILE"
+ hello_done=0
+ else
+ tls_hello_ascii+="$next_packet"
+ if [[ $DEBUG -ge 1 ]]; then
+ sock_reply_file3=${SOCK_REPLY_FILE}.3
+ mv "$SOCK_REPLY_FILE" "$sock_reply_file3" #FIXME: we moved that already
+ mv "$sock_reply_file2" "$SOCK_REPLY_FILE"
+ cat "$sock_reply_file3" >> "$SOCK_REPLY_FILE"
+ rm "$sock_reply_file3"
+ fi
+
+ check_tls_serverhellodone "$tls_hello_ascii" "ephemeralkey"
+ hello_done=$?
+ fi
+ done
+
+ debugme echo "reading server hello..."
+ if [[ "$DEBUG" -ge 4 ]]; then
+ hexdump -C $SOCK_REPLY_FILE | head -6
+ echo
+ fi
+ if [[ "${tls_hello_ascii:0:1}" == 8 ]]; then
+ parse_sslv2_serverhello "$SOCK_REPLY_FILE" "false"
+ if [[ $? -eq 3 ]] && [[ "$V2_HELLO_CIPHERSPEC_LENGTH" -ne 0 ]]; then
+ echo "Protocol : SSLv2" > "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt"
+ DETECTED_TLS_VERSION="0200"
+ ret=0
+ else
+ ret=1
+ fi
+ else
+ parse_tls_serverhello "$tls_hello_ascii" "ephemeralkey" "$cipher_list_2send"
+ save=$?
+
+ if [[ $save -eq 0 ]]; then
+ send_close_notify "$DETECTED_TLS_VERSION"
+ fi
+
+ if [[ $DEBUG -ge 2 ]]; then
+ # see https://secure.wand.net.nz/trac/libprotoident/wiki/SSL
+ lines=$(count_lines "$(hexdump -C "$SOCK_REPLY_FILE" 2>$ERRFILE)")
+ tm_out " ($lines lines returned) "
+ fi
+
+ # determine the return value for higher level, so that they can tell what the result is
+ if [[ $save -eq 1 ]] || [[ $lines -eq 1 ]]; then
+ ret=1 # NOT available
+ else
+ ret=0
+ fi
+ debugme tmln_out
+ fi
+
+ close_socket
+ TMPFILE=$SOCK_REPLY_FILE
+ tmpfile_handle ${FUNCNAME[0]}.dd
+ return $ret
+}
+
+run_client_simulation() {
+ # Runs browser simulations. Browser capabilities gathered from:
+ # https://www.ssllabs.com/ssltest/clients.html on 10 jan 2016
+ local names=()
+ local short=()
+ local protos=()
+ local ciphers=()
+ local ciphersuites=()
+ local tlsvers=()
+ local sni=()
+ local warning=()
+ local handshakebytes=()
+ local lowest_protocol=()
+ local highest_protocol=()
+ local service=()
+ local minDhBits=()
+ local maxDhBits=()
+ local minRsaBits=()
+ local maxRsaBits=()
+ local minEcdsaBits=()
+ local curves=()
+ local requiresSha2=()
+ local current=()
+ local i=0
+ local name tls proto cipher temp what_dh bits curve supported_curves
+ local has_dh_bits using_sockets=true
+ local client_service
+ local options
+ local -i ret=0
+ local jsonID="clientsimulation"
+ local client_service=""
+
+ # source the external file
+ . "$TESTSSL_INSTALL_DIR/etc/client-simulation.txt" 2>/dev/null
+ if [[ $? -ne 0 ]]; then
+ prln_local_problem "couldn't find client simulation data in $TESTSSL_INSTALL_DIR/etc/client-simulation.txt"
+ return 1
+ fi
+
+ "$SSL_NATIVE" && using_sockets=false
+
+ if [[ $SERVICE != "" ]]; then
+ client_service="$SERVICE"
+ elif [[ -n "$STARTTLS_PROTOCOL" ]]; then
+ # Can we take the service from STARTTLS?
+ client_service=$(toupper "${STARTTLS_PROTOCOL%s}") # strip trailing 's' in ftp(s), smtp(s), pop3(s), etc
+ elif "$ASSUME_HTTP"; then
+ client_service="HTTP"
+ else
+ outln "Could not determine the protocol, only simulating generic clients."
+ fi
+
+ outln
+ pr_headline " Running client simulations "
+ [[ "$client_service" == HTTP ]] && pr_headline "($client_service) "
+ if "$using_sockets"; then
+ pr_headlineln "via sockets "
+ else
+ pr_headline "via openssl "
+ prln_warning " -- pls note \"--ssl-native\" will return some false results"
+ fileout "$jsonID" "WARN" "You shouldn't run this with \"--ssl-native\" as you will get false results"
+ ret=1
+ fi
+ outln
+ debugme echo
+
+ if "$WIDE"; then
+ if [[ "$DISPLAY_CIPHERNAMES" =~ openssl ]]; then
+ out " Browser Protocol Cipher Suite Name (OpenSSL) "
+ ( "$using_sockets" || "$HAS_DH_BITS") && out "Forward Secrecy"
+ outln
+ out "--------------------------------------------------------------------------"
+ else
+ out " Browser Protocol Cipher Suite Name (IANA/RFC) "
+ ( "$using_sockets" || "$HAS_DH_BITS") && out "Forward Secrecy"
+ outln
+ out "------------------------------------------------------------------------------------------"
+ fi
+ ( "$using_sockets" || "$HAS_DH_BITS") && out "----------------------"
+ outln
+ fi
+ if ! "$using_sockets"; then
+ # We can't use the connectivity checker here as of now the openssl reply is always empty (reason??)
+ save_max_ossl_fail=$MAX_OSSL_FAIL
+ nr_ossl_fail=$NR_OSSL_FAIL
+ MAX_OSSL_FAIL=100
+ fi
+ for name in "${short[@]}"; do
+ if "${current[i]}" || "$ALL_CLIENTS" ; then
+ # for ANY we test this service or if the service we determined from STARTTLS matches
+ if [[ "${service[i]}" == ANY ]] || [[ "${service[i]}" =~ $client_service ]]; then
+ out " $(printf -- "%-29s" "${names[i]}")"
+ if "$using_sockets" && [[ -n "${handshakebytes[i]}" ]]; then
+ client_simulation_sockets "${handshakebytes[i]}"
+ sclient_success=$?
+ if [[ $sclient_success -eq 0 ]]; then
+ if [[ "0x${DETECTED_TLS_VERSION}" -lt ${lowest_protocol[i]} ]] || \
+ [[ "0x${DETECTED_TLS_VERSION}" -gt ${highest_protocol[i]} ]]; then
+ sclient_success=1
+ fi
+ [[ $sclient_success -eq 0 ]] && cp "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt" $TMPFILE >$ERRFILE
+ fi
+ else
+ if [[ -n "${curves[i]}" ]]; then
+ # "$OPENSSL s_client" will fail if the -curves option includes any unsupported curves.
+ supported_curves=""
+ for curve in $(colon_to_spaces "${curves[i]}"); do
+ # Attention! secp256r1 = prime256v1 and secp192r1 = prime192v1
+ # We need to map two curves here as otherwise handshakes will go wrong if "-curves" are supplied
+ # https://github.com/openssl/openssl/blob/master/apps/ecparam.c#L221 + ./ssl/t1_lib.c
+ [[ "$curve" =~ secp256r1 ]] && curve="${curve//secp256r1/prime256v1}"
+ [[ "$curve" =~ secp192r1 ]] && curve="${curve//secp192r1/prime192v1}"
+ [[ "$OSSL_SUPPORTED_CURVES" =~ " $curve " ]] && supported_curves+=":$curve"
+ done
+ curves[i]=""
+ [[ -n "$supported_curves" ]] && curves[i]="-curves ${supported_curves:1}"
+ fi
+ options="$(s_client_options "-cipher ${ciphers[i]} -ciphersuites "\'${ciphersuites[i]}\'" ${curves[i]} ${protos[i]} $STARTTLS $BUGS $PROXY -connect $NODEIP:$PORT ${sni[i]}")"
+ debugme echo "$OPENSSL s_client $options </dev/null"
+ $OPENSSL s_client $options </dev/null >$TMPFILE 2>$ERRFILE
+ sclient_connect_successful $? $TMPFILE
+ sclient_success=$?
+ fi
+ if [[ $sclient_success -eq 0 ]]; then
+ # If an ephemeral DH key was used, check that the number of bits is within range.
+ temp=$(awk -F': ' '/^Server Temp Key/ { print $2 }' "$TMPFILE") # extract line
+ what_dh="${temp%%,*}"
+ bits="${temp##*, }"
+ # formatting
+ curve="${temp#*, }"
+ if [[ "$curve" == $bits ]]; then
+ curve=""
+ else
+ curve="${curve%%,*}"
+ fi
+ bits="${bits/bits/}"
+ bits="${bits// /}"
+ if [[ "$what_dh" == X25519 ]] || [[ "$what_dh" == X448 ]]; then
+ curve="$what_dh"
+ what_dh="ECDH"
+ fi
+ if [[ "$what_dh" == DH ]]; then
+ [[ ${minDhBits[i]} -ne -1 ]] && [[ $bits -lt ${minDhBits[i]} ]] && sclient_success=1
+ [[ ${maxDhBits[i]} -ne -1 ]] && [[ $bits -gt ${maxDhBits[i]} ]] && sclient_success=1
+ fi
+ fi
+ if [[ $sclient_success -ne 0 ]]; then
+ outln "No connection"
+ fileout "${jsonID}-${short[i]}" "INFO" "No connection"
+ else
+ proto=$(get_protocol $TMPFILE)
+ # hack:
+ [[ "$proto" == TLSv1 ]] && proto="TLSv1.0"
+ [[ "$proto" == SSLv3 ]] && proto="SSLv3 "
+ if [[ "$proto" == TLSv1.2 ]] && ( ! "$using_sockets" || [[ -z "${handshakebytes[i]}" ]] ); then
+ # OpenSSL reports TLS1.2 even if the connection is TLS1.1 or TLS1.0. Need to figure out which one it is...
+ for tls in ${tlsvers[i]}; do
+ # If the handshake data includes TLS 1.3 we need to remove it, otherwise the
+ # simulation will fail with # 'Oops: openssl s_client connect problem'
+ # before/after trying another protocol. We only print a warning it in debug mode
+ # as otherwise we would need e.g. handle the curves in a similar fashion -- not
+ # to speak about ciphers
+ if [[ $tls =~ 1_3 ]] && ! "$HAS_TLS13"; then
+ debugme pr_local_problem "TLS 1.3 not supported, "
+ continue
+ fi
+ options="$(s_client_options "$tls -cipher ${ciphers[i]} -ciphersuites "\'${ciphersuites[i]}\'" ${curves[i]} $STARTTLS $BUGS $PROXY -connect $NODEIP:$PORT ${sni[i]}")"
+ debugme echo "$OPENSSL s_client $options </dev/null"
+ $OPENSSL s_client $options </dev/null >$TMPFILE 2>$ERRFILE
+ sclient_connect_successful $? $TMPFILE
+ sclient_success=$?
+ if [[ $sclient_success -eq 0 ]]; then
+ case "$tls" in
+ "-tls1_2") break ;;
+ "-tls1_1") proto="TLSv1.1"
+ break ;;
+ "-tls1") proto="TLSv1.0"
+ break ;;
+ esac
+ fi
+ done
+ fi
+ cipher=$(get_cipher $TMPFILE)
+ if [[ "$DISPLAY_CIPHERNAMES" =~ openssl ]] && ( [[ "$cipher" == TLS_* ]] || [[ "$cipher" == SSL_* ]] ); then
+ cipher="$(rfc2openssl "$cipher")"
+ [[ -z "$cipher" ]] && cipher=$(get_cipher $TMPFILE)
+ elif [[ "$DISPLAY_CIPHERNAMES" =~ rfc ]] && [[ "$cipher" != TLS_* ]] && [[ "$cipher" != SSL_* ]]; then
+ cipher="$(openssl2rfc "$cipher")"
+ [[ -z "$cipher" ]] && cipher=$(get_cipher $TMPFILE)
+ fi
+ out "$proto "
+ "$WIDE" && out " "
+ if [[ "$COLOR" -le 2 ]]; then
+ out "$cipher"
+ else
+ pr_cipher_quality "$cipher"
+ fi
+ if "$WIDE"; then
+ if [[ "$DISPLAY_CIPHERNAMES" =~ openssl ]]; then
+ for (( j=${#cipher}; j < 34; j++ )); do
+ out " "
+ done
+ else
+ for (( j=${#cipher}; j < 50; j++ )); do
+ out " "
+ done
+ fi
+ fi
+ if [[ -n "$what_dh" ]]; then
+ [[ -n "$curve" ]] && curve="($curve)"
+ "$WIDE" || out ", "
+ if [[ "$what_dh" == ECDH ]]; then
+ pr_ecdh_quality "$bits" "$(printf -- "%-12s" "$bits bit $what_dh") $curve"
+ else
+ pr_dh_quality "$bits" "$(printf -- "%-12s" "$bits bit $what_dh") $curve"
+ fi
+ else
+ if "$HAS_DH_BITS" || ( "$using_sockets" && [[ -n "${handshakebytes[i]}" ]] ); then
+ "$WIDE" || out ", "
+ out "No FS"
+ fi
+ fi
+ outln
+ if [[ -n "${warning[i]}" ]]; then
+ out " "
+ outln "${warning[i]}"
+ fi
+ fileout "${jsonID}-${short[i]}" "INFO" "$proto $cipher ${warning[i]}"
+ debugme cat $TMPFILE
+ fi
+ fi # correct service?
+ fi #current?
+ ((i++))
+ done
+ if ! "$using_sockets"; then
+ # restore from above
+ MAX_OSSL_FAIL=$save_max_ossl_fail
+ NR_OSSL_FAIL=$nr_ossl_fail
+ fi
+
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return $ret
+}
+
+# generic function whether $1 is supported by s_client ($2: string to display, currently nowhere being used)
+#
+locally_supported() {
+ [[ -n "$2" ]] && out "$2 "
+ if $OPENSSL s_client "$1" -connect invalid. 2>&1 | grep -aiq "unknown option"; then
+ prln_local_problem "$OPENSSL doesn't support \"s_client $1\""
+ return 7
+ fi
+ return 0
+}
+
+
+# The protocol check in run_protocols needs to be redone. The using_sockets part there kind of sucks.
+# 1) we need to have a variable where the results are being stored so that every other test doesn't have to do this again
+# --> we have that but certain information like "downgraded" are not being passed. That's not ok for run_protocols()/
+# for all other functions we can use it
+# 2) the code is old and one can do that way better
+# We should do what's available and faster (openssl vs. sockets). Keep in mind that the socket reply for SSLv2 returns the number # of ciphers!
+#
+# arg1: -ssl2|-ssl3|-tls1|-tls1_1|-tls1_2|-tls1_3
+#
+run_prototest_openssl() {
+ local -i ret=0
+ local protos proto
+
+ # check whether the protocol being tested is supported by $OPENSSL
+ $OPENSSL s_client "$1" -connect invalid. 2>&1 | grep -aiq "unknown option" && return 7
+ case "$1" in
+ -ssl2) protos="-ssl2" ;;
+ -ssl3) protos="-ssl3" ;;
+ -tls1) protos="-no_tls1_2 -no_tls1_1 -no_ssl2"; "$HAS_TLS13" && protos+=" -no_tls1_3" ;;
+ -tls1_1) protos="-no_tls1_2 -no_ssl2"; "$HAS_TLS13" && protos+=" -no_tls1_3" ;;
+ -tls1_2) protos="-no_ssl2"; "$HAS_TLS13" && protos+=" -no_tls1_3" ;;
+ -tls1_3) protos="" ;;
+ esac
+ $OPENSSL s_client $(s_client_options "-state $protos $STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY $SNI") >$TMPFILE 2>&1 </dev/null
+ sclient_connect_successful $? $TMPFILE
+ ret=$?
+ debugme grep -E "error|failure" $ERRFILE | grep -Eav "unable to get local|verify error"
+ if [[ $ret -ne 0 ]]; then
+ if grep -aq "no cipher list" $TMPFILE; then
+ ret=5 # <--- important indicator for SSL2 (maybe others, too)
+ else
+ # try again without $PROXY
+ $OPENSSL s_client $(s_client_options "-state $protos $STARTTLS $BUGS -connect $NODEIP:$PORT $SNI") >$TMPFILE 2>&1 </dev/null
+ sclient_connect_successful $? $TMPFILE
+ ret=$?
+ debugme grep -E "error|failure" $ERRFILE | grep -Eav "unable to get local|verify error"
+ grep -aq "no cipher list" $TMPFILE && ret=5 # <--- important indicator for SSL2 (maybe others, too)
+ fi
+ fi
+ if [[ $ret -eq 0 ]]; then
+ proto="$(get_protocol "$TMPFILE")"
+ proto=${proto/\./_}
+ proto=${proto/v/}
+ proto="-$(tolower $proto)"
+ [[ "$proto" != $1 ]] && ret=2
+ case "$proto" in
+ -ssl3) DETECTED_TLS_VERSION="0300" ;;
+ -tls1) DETECTED_TLS_VERSION="0301" ;;
+ -tls1_1) DETECTED_TLS_VERSION="0302" ;;
+ -tls1_2) DETECTED_TLS_VERSION="0303" ;;
+ -tls1_3) DETECTED_TLS_VERSION="0304" ;;
+ esac
+ fi
+ tmpfile_handle ${FUNCNAME[0]}$1.txt
+ return $ret
+
+ # 0: offered
+ # 1: not offered
+ # 2: downgraded
+ # 5: protocol ok, but no cipher
+ # 7: no local support
+}
+
+# Idempotent function to add SSL/TLS protocols. It should accelerate testing.
+# PROTOS_OFFERED can be e.g. "ssl2:no ssl3:no tls1_2:yes" which means that
+# SSLv2 and SSLv3 was tested but not available, TLS 1.2 was tested and available
+# TLS 1.0 and TLS 1.2 not tested yet
+#
+# arg1: protocol
+# arg2: available (yes) or not (no)
+add_tls_offered() {
+ # the ":" is mandatory here (and @ other places), otherwise e.g. tls1 will match tls1_2
+ if [[ "$PROTOS_OFFERED" =~ $1: ]]; then
+ # we got that protocol already
+ :
+ else
+ PROTOS_OFFERED+="${1}:$2 "
+ fi
+}
+
+# function which checks whether SSLv2 - TLS 1.2 is being offered, see add_tls_offered()
+# arg1: protocol string or hex code for TLS protocol
+# echos: 0 if proto known being offered, 1: known not being offered, 2: we don't know yet whether proto is being offered
+# return value is always zero
+has_server_protocol() {
+ local proto
+ local proto_val_pair
+
+ case "$1" in
+ 04) proto="tls1_3" ;;
+ 03) proto="tls1_2" ;;
+ 02) proto="tls1_1" ;;
+ 01) proto="tls1" ;;
+ 00) proto="ssl3" ;;
+ *) proto="$1" ;;
+ esac
+
+ if [[ "$PROTOS_OFFERED" =~ $proto: ]]; then
+ for proto_val_pair in $PROTOS_OFFERED; do
+ if [[ $proto_val_pair =~ $proto: ]]; then
+ if [[ ${proto_val_pair#*:} == yes ]]; then
+ echo 0
+ return 0
+ else
+ echo 1
+ return 0
+ fi
+ fi
+ done
+ else
+ # if empty echo 2, hinting to the caller to check at additional cost/connect
+ echo 2
+ return 0
+ fi
+}
+
+
+# the protocol check needs to be revamped. It sucks, see above
+run_protocols() {
+ local using_sockets=true
+ local supported_no_ciph1="supported but couldn't detect a cipher (may need debugging)"
+ local supported_no_ciph2="supported but couldn't detect a cipher"
+ local latest_supported="" # version.major and version.minor of highest version supported by the server
+ local detected_version_string latest_supported_string
+ local key_share_extn_nr="$KEY_SHARE_EXTN_NR"
+ local lines nr_ciphers_detected
+ local tls13_ciphers_to_test=""
+ local i drafts_offered="" drafts_offered_str="" supported_versions debug_recomm=""
+ local tls12_detected_version
+ local -i ret=0 ret_val_ssl3 ret_val_tls1 ret_val_tls11 ret_val_tls12=0 ret_val_tls13=0
+ local offers_tls13=false
+ local jsonID="SSLv2"
+
+ outln; pr_headline " Testing protocols "
+
+ if "$SSL_NATIVE"; then
+ using_sockets=false
+ prln_underline "via native openssl"
+ else
+ using_sockets=true
+ if [[ -n "$STARTTLS" ]]; then
+ prln_underline "via sockets "
+ else
+ prln_underline "via sockets except NPN+ALPN "
+ fi
+ fi
+ outln
+ [[ "$DEBUG" -le 1 ]] && debug_recomm=", rerun with DEBUG>=2 or --ssl-native"
+
+ pr_bold " SSLv2 ";
+ if ! "$SSL_NATIVE"; then
+ sslv2_sockets
+ case $? in
+ 6) # couldn't open socket
+ prln_fixme "couldn't open socket"
+ fileout "$jsonID" "WARN" "couldn't be tested, socket problem"
+ ((ret++))
+ ;;
+ 7) # strange reply, couldn't convert the cipher spec length to a hex number
+ pr_cyan "strange v2 reply "
+ outln "$debug_recomm"
+ [[ $DEBUG -ge 3 ]] && hexdump -C "$TEMPDIR/$NODEIP.sslv2_sockets.dd" | head -1
+ fileout "$jsonID" "WARN" "received a strange SSLv2 reply (rerun with DEBUG>=2)"
+ ;;
+ 1) # no sslv2 server hello returned, like in openlitespeed which returns HTTP!
+ prln_svrty_best "not offered (OK)"
+ fileout "$jsonID" "OK" "not offered"
+ add_tls_offered ssl2 no
+ ;;
+ 0) # reset
+ prln_svrty_best "not offered (OK)"
+ fileout "$jsonID" "OK" "not offered"
+ add_tls_offered ssl2 no
+ ;;
+ 4) out "likely "; pr_svrty_best "not offered (OK), "
+ fileout "$jsonID" "OK" "likely not offered"
+ add_tls_offered ssl2 no
+ pr_warning "received 4xx/5xx after STARTTLS handshake"; outln "$debug_recomm"
+ fileout "$jsonID" "WARN" "received 4xx/5xx after STARTTLS handshake${debug_recomm}"
+ ;;
+ 3) lines=$(count_lines "$(hexdump -C "$TEMPDIR/$NODEIP.sslv2_sockets.dd" 2>/dev/null)")
+ [[ "$DEBUG" -ge 2 ]] && tm_out " ($lines lines) "
+ if [[ "$lines" -gt 1 ]]; then
+ nr_ciphers_detected=$((V2_HELLO_CIPHERSPEC_LENGTH / 3))
+ add_tls_offered ssl2 yes
+ if [[ 0 -eq "$nr_ciphers_detected" ]]; then
+ prln_svrty_high "supported but couldn't detect a cipher and vulnerable to CVE-2015-3197 ";
+ fileout "$jsonID" "HIGH" "offered, no cipher" "CVE-2015-3197" "CWE-310"
+ else
+ pr_svrty_critical "offered (NOT ok), also VULNERABLE to DROWN attack";
+ outln " -- $nr_ciphers_detected ciphers"
+ fileout "$jsonID" "CRITICAL" "vulnerable with $nr_ciphers_detected ciphers"
+ fi
+ fi
+ ;;
+ *) pr_fixme "unexpected value around line $((LINENO))"; outln "$debug_recomm"
+ ((ret++))
+ ;;
+ esac
+ debugme tmln_out
+ else
+ run_prototest_openssl "-ssl2"
+ case $? in
+ 0) prln_svrty_critical "offered (NOT ok)"
+ fileout "$jsonID" "CRITICAL" "offered"
+ add_tls_offered ssl2 yes
+ ;;
+ 1) prln_svrty_best "not offered (OK)"
+ fileout "$jsonID" "OK" "not offered"
+ add_tls_offered ssl2 no
+ ;;
+ 5) prln_svrty_high "CVE-2015-3197: $supported_no_ciph2";
+ fileout "$jsonID" "HIGH" "offered, no cipher" "CVE-2015-3197" "CWE-310"
+ add_tls_offered ssl2 yes
+ ;;
+ 7) prln_local_problem "$OPENSSL doesn't support \"s_client -ssl2\""
+ fileout "$jsonID" "INFO" "not tested due to lack of local support"
+ ((ret++))
+ ;;
+ esac
+ fi
+
+ pr_bold " SSLv3 ";
+ jsonID="SSLv3"
+ if [[ $(has_server_protocol ssl3) -eq 0 ]]; then
+ ret_val_ssl3=0
+ elif "$using_sockets"; then
+ tls_sockets "00" "$TLS_CIPHER"
+ ret_val_ssl3=$?
+ else
+ run_prototest_openssl "-ssl3"
+ ret_val_ssl3=$?
+ fi
+ case $ret_val_ssl3 in
+ 0) prln_svrty_high "offered (NOT ok)"
+ fileout "$jsonID" "HIGH" "offered"
+ if "$using_sockets" || "$HAS_SSL3"; then
+ latest_supported="0300"
+ latest_supported_string="SSLv3"
+ fi
+ add_tls_offered ssl3 yes
+ ;;
+ 1) prln_svrty_best "not offered (OK)"
+ fileout "$jsonID" "OK" "not offered"
+ add_tls_offered ssl3 no
+ ;;
+ 2) if [[ "$DETECTED_TLS_VERSION" == 03* ]]; then
+ detected_version_string="TLSv1.$((0x$DETECTED_TLS_VERSION-0x0301))"
+ prln_svrty_critical "server responded with higher version number ($detected_version_string) than requested by client (NOT ok)"
+ fileout "$jsonID" "CRITICAL" "server responded with higher version number ($detected_version_string) than requested by client"
+ else
+ if [[ ${#DETECTED_TLS_VERSION} -eq 4 ]]; then
+ prln_svrty_critical "server responded with version number ${DETECTED_TLS_VERSION:0:2}.${DETECTED_TLS_VERSION:2:2} (NOT ok)"
+ fileout "$jsonID" "CRITICAL" "server responded with version number ${DETECTED_TLS_VERSION:0:2}.${DETECTED_TLS_VERSION:2:2}"
+ else
+ prln_svrty_medium "strange, server ${DETECTED_TLS_VERSION}"
+ fileout "$jsonID" "MEDIUM" "strange, server ${DETECTED_TLS_VERSION}"
+ ((ret++))
+ fi
+ fi
+ ;;
+ 3) pr_svrty_best "not offered (OK), "
+ fileout "$jsonID" "OK" "not offered"
+ add_tls_offered ssl3 no
+ pr_warning "SSL downgraded to STARTTLS plaintext"; outln
+ fileout "$jsonID" "WARN" "SSL downgraded to STARTTLS plaintext"
+ ;;
+ 4) out "likely "; pr_svrty_best "not offered (OK), "
+ fileout "$jsonID" "OK" "not offered"
+ add_tls_offered ssl3 no
+ pr_warning "received 4xx/5xx after STARTTLS handshake"; outln "$debug_recomm"
+ fileout "$jsonID" "WARN" "received 4xx/5xx after STARTTLS handshake${debug_recomm}"
+ ;;
+ 5) pr_svrty_high "$supported_no_ciph1" # protocol detected but no cipher --> comes from run_prototest_openssl
+ fileout "$jsonID" "HIGH" "$supported_no_ciph1"
+ add_tls_offered ssl3 yes
+ ;;
+ 7) if "$using_sockets" ; then
+ # can only happen in debug mode
+ pr_warning "strange reply, maybe a client side problem with SSLv3"; outln "$debug_recomm"
+ else
+ prln_local_problem "$OPENSSL doesn't support \"s_client -ssl3\""
+ fileout "$jsonID" "WARN" "not tested due to lack of local support"
+ fi
+ ;;
+ *) pr_fixme "unexpected value around line $((LINENO))"; outln "$debug_recomm"
+ ((ret++))
+ ;;
+ esac
+
+ pr_bold " TLS 1 ";
+ jsonID="TLS1"
+ if [[ $(has_server_protocol tls1) -eq 0 ]]; then
+ ret_val_tls1=0
+ elif "$using_sockets"; then
+ tls_sockets "01" "$TLS_CIPHER"
+ ret_val_tls1=$?
+ else
+ run_prototest_openssl "-tls1"
+ ret_val_tls1=$?
+ fi
+ case $ret_val_tls1 in
+ 0) pr_svrty_low "offered" ; outln " (deprecated)"
+ fileout "$jsonID" "LOW" "offered (deprecated)"
+ latest_supported="0301"
+ latest_supported_string="TLSv1.0"
+ add_tls_offered tls1 yes
+ ;; # nothing wrong with it -- per se
+ 1) out "not offered"
+ add_tls_offered tls1 no
+ if [[ -z $latest_supported ]]; then
+ outln
+ fileout "$jsonID" "INFO" "not offered" # neither good or bad
+ else
+ prln_svrty_critical " -- connection failed rather than downgrading to $latest_supported_string (NOT ok)"
+ fileout "$jsonID" "CRITICAL" "connection failed rather than downgrading to $latest_supported_string"
+ fi
+ ;;
+ 2) pr_svrty_medium "not offered"
+ add_tls_offered tls1 no
+ if [[ "$DETECTED_TLS_VERSION" == 0300 ]]; then
+ [[ $DEBUG -ge 1 ]] && tm_out " -- downgraded"
+ outln
+ fileout "$jsonID" "MEDIUM" "not offered, and downgraded to SSL"
+ elif [[ "$DETECTED_TLS_VERSION" == 03* ]]; then
+ detected_version_string="TLSv1.$((0x$DETECTED_TLS_VERSION-0x0301))"
+ prln_svrty_critical " -- server responded with higher version number ($detected_version_string) than requested by client"
+ fileout "$jsonID" "CRITICAL" "server responded with higher version number ($detected_version_string) than requested by client"
+ else
+ if [[ ${#DETECTED_TLS_VERSION} -eq 4 ]]; then
+ prln_svrty_critical "server responded with version number ${DETECTED_TLS_VERSION:0:2}.${DETECTED_TLS_VERSION:2:2} (NOT ok)"
+ fileout "$jsonID" "CRITICAL" "server responded with version number ${DETECTED_TLS_VERSION:0:2}.${DETECTED_TLS_VERSION:2:2}"
+ else
+ prln_svrty_medium " -- strange, server ${DETECTED_TLS_VERSION}"
+ fileout "$jsonID" "MEDIUM" "strange, server ${DETECTED_TLS_VERSION}"
+ fi
+ fi
+ ;;
+ 3) out "not offered, "
+ fileout "$jsonID" "OK" "not offered"
+ add_tls_offered tls1 no
+ pr_warning "TLS downgraded to STARTTLS plaintext"; outln
+ fileout "$jsonID" "WARN" "TLS downgraded to STARTTLS plaintext"
+ ;;
+ 4) out "likely not offered, "
+ fileout "$jsonID" "INFO" "likely not offered"
+ add_tls_offered tls1 no
+ pr_warning "received 4xx/5xx after STARTTLS handshake"; outln "$debug_recomm"
+ fileout "$jsonID" "WARN" "received 4xx/5xx after STARTTLS handshake${debug_recomm}"
+ ;;
+ 5) outln "$supported_no_ciph1" # protocol detected but no cipher --> comes from run_prototest_openssl
+ fileout "$jsonID" "INFO" "$supported_no_ciph1"
+ add_tls_offered tls1 yes
+ ;;
+ 7) if "$using_sockets" ; then
+ # can only happen in debug mode
+ pr_warning "strange reply, maybe a client side problem with TLS 1.0"; outln "$debug_recomm"
+ else
+ prln_local_problem "$OPENSSL doesn't support \"s_client -tls1\""
+ fileout "$jsonID" "WARN" "not tested due to lack of local support"
+ fi
+ ((ret++))
+ ;;
+ *) pr_fixme "unexpected value around line $((LINENO))"; outln "$debug_recomm"
+ ((ret++))
+ ;;
+ esac
+
+ pr_bold " TLS 1.1 ";
+ jsonID="TLS1_1"
+ if [[ $(has_server_protocol tls1_1) -eq 0 ]]; then
+ ret_val_tls11=0
+ elif "$using_sockets"; then
+ tls_sockets "02" "$TLS_CIPHER"
+ ret_val_tls11=$?
+ else
+ run_prototest_openssl "-tls1_1"
+ ret_val_tls11=$?
+ fi
+ case $ret_val_tls11 in
+ 0) pr_svrty_low "offered" ; outln " (deprecated)"
+ fileout "$jsonID" "LOW" "offered (deprecated)"
+ latest_supported="0302"
+ latest_supported_string="TLSv1.1"
+ add_tls_offered tls1_1 yes
+ ;; # nothing wrong with it
+ 1) out "not offered"
+ add_tls_offered tls1_1 no
+ if [[ -z $latest_supported ]]; then
+ outln
+ fileout "$jsonID" "INFO" "not offered" # neither good or bad
+ else
+ prln_svrty_critical " -- connection failed rather than downgrading to $latest_supported_string"
+ fileout "$jsonID" "CRITICAL" "connection failed rather than downgrading to $latest_supported_string"
+ fi
+ ;;
+ 2) out "not offered"
+ add_tls_offered tls1_1 no
+ if [[ "$DETECTED_TLS_VERSION" == "$latest_supported" ]]; then
+ [[ $DEBUG -ge 1 ]] && tm_out " -- downgraded"
+ outln
+ fileout "$jsonID" "CRITICAL" "TLSv1.1 is not offered, and downgraded to a weaker protocol"
+ elif [[ "$DETECTED_TLS_VERSION" == 0300 ]] && [[ "$latest_supported" == 0301 ]]; then
+ prln_svrty_critical " -- server supports TLSv1.0, but downgraded to SSLv3 (NOT ok)"
+ fileout "$jsonID" "CRITICAL" "not offered, and downgraded to SSLv3 rather than TLSv1.0"
+ elif [[ "$DETECTED_TLS_VERSION" == 03* ]] && [[ 0x$DETECTED_TLS_VERSION -gt 0x0302 ]]; then
+ detected_version_string="TLSv1.$((0x$DETECTED_TLS_VERSION-0x0301))"
+ prln_svrty_critical " -- server responded with higher version number ($detected_version_string) than requested by client (NOT ok)"
+ fileout "$jsonID" "CRITICAL" "not offered, server responded with higher version number ($detected_version_string) than requested by client"
+ else
+ if [[ ${#DETECTED_TLS_VERSION} -eq 4 ]]; then
+ prln_svrty_critical "server responded with version number ${DETECTED_TLS_VERSION:0:2}.${DETECTED_TLS_VERSION:2:2} (NOT ok)"
+ fileout "$jsonID" "CRITICAL" "server responded with version number ${DETECTED_TLS_VERSION:0:2}.${DETECTED_TLS_VERSION:2:2}"
+ else
+ prln_svrty_medium " -- strange, server ${DETECTED_TLS_VERSION}"
+ fileout "$jsonID" "MEDIUM" "strange, server ${DETECTED_TLS_VERSION}"
+ fi
+ fi
+ ;;
+ 3) out "not offered, "
+ fileout "$jsonID" "OK" "not offered"
+ add_tls_offered tls1_1 no
+ pr_warning "TLS downgraded to STARTTLS plaintext"; outln
+ fileout "$jsonID" "WARN" "TLS downgraded to STARTTLS plaintext"
+ ;;
+ 4) out "likely not offered, "
+ fileout "$jsonID" "INFO" "not offered"
+ add_tls_offered tls1_1 no
+ pr_warning "received 4xx/5xx after STARTTLS handshake"; outln "$debug_recomm"
+ fileout "$jsonID" "WARN" "received 4xx/5xx after STARTTLS handshake${debug_recomm}"
+ ;;
+ 5) outln "$supported_no_ciph1" # protocol detected but no cipher --> comes from run_prototest_openssl
+ fileout "$jsonID" "INFO" "$supported_no_ciph1"
+ add_tls_offered tls1_1 yes
+ ;;
+ 7) if "$using_sockets" ; then
+ # can only happen in debug mode
+ pr_warning "strange reply, maybe a client side problem with TLS 1.1"; outln "$debug_recomm"
+ else
+ prln_local_problem "$OPENSSL doesn't support \"s_client -tls1_1\""
+ fileout "$jsonID" "WARN" "not tested due to lack of local support"
+ fi
+ ((ret++))
+ ;;
+ *) pr_fixme "unexpected value around line $((LINENO))"; outln "$debug_recomm"
+ ((ret++))
+ ;;
+ esac
+
+ # Now, we are doing a basic/pre test for TLS 1.2 and 1.3 in order not to penalize servers (medium)
+ # running TLS 1.3 only when TLS 1.2 is not offered. 0 and 5 are the return codes for
+ # TLS 1.3 support (kind of, including deprecated pre-versions of TLS 1.3)
+ if [[ $(has_server_protocol tls1_2) -eq 0 ]]; then
+ ret_val_tls12=0
+ elif "$using_sockets"; then
+ tls_sockets "03" "$TLS12_CIPHER"
+ ret_val_tls12=$?
+ tls12_detected_version="$DETECTED_TLS_VERSION"
+ else
+ run_prototest_openssl "-tls1_2"
+ ret_val_tls12=$?
+ tls12_detected_version="$DETECTED_TLS_VERSION"
+ fi
+
+ if [[ $(has_server_protocol tls1_3) -eq 0 ]]; then
+ ret_val_tls13=0
+ elif "$using_sockets"; then
+ # Need to ensure that at most 128 ciphers are included in ClientHello.
+ # If the TLSv1.2 test in determine_optimal_sockets_params() was successful,
+ # then use the 5 TLSv1.3 ciphers plus the cipher selected in the TLSv1.2 test.
+ # If the TLSv1.2 test was not successful, then just use the 5 TLSv1.3 ciphers
+ # plus the list of ciphers used in all of the previous tests ($TLS_CIPHER).
+ if [[ -n "$TLS12_CIPHER_OFFERED" ]]; then
+ tls13_ciphers_to_test="$TLS13_CIPHER, $TLS12_CIPHER_OFFERED, 00,ff"
+ else
+ tls13_ciphers_to_test="$TLS13_CIPHER,$TLS_CIPHER"
+ fi
+ tls_sockets "04" "$tls13_ciphers_to_test"
+ ret_val_tls13=$?
+ else
+ run_prototest_openssl "-tls1_3"
+ ret_val_tls13=$?
+ fi
+ if [[ $ret_val_tls13 -eq 0 ]] || [[ $ret_val_tls13 -eq 5 ]]; then
+ offers_tls13=true # This variable comes in handy for further if statements below
+ fi
+ # Done with pretesting TLS 1.2 and 1.3.
+
+ pr_bold " TLS 1.2 ";
+ jsonID="TLS1_2"
+ case $ret_val_tls12 in
+ 0) prln_svrty_best "offered (OK)"
+ fileout "$jsonID" "OK" "offered"
+ latest_supported="0303"
+ latest_supported_string="TLSv1.2"
+ add_tls_offered tls1_2 yes
+ ;; # GCM cipher in TLS 1.2: very good!
+ 1) add_tls_offered tls1_2 no
+ if "$offers_tls13"; then
+ out "not offered"
+ else
+ pr_svrty_medium "not offered"
+ fi
+ if [[ -z $latest_supported ]]; then
+ outln
+ if "$offers_tls13"; then
+ fileout "$jsonID" "INFO" "not offered"
+ else
+ fileout "$jsonID" "MEDIUM" "not offered" # TLS 1.3, no TLS 1.2 --> no GCM, penalty
+ fi
+ else
+ prln_svrty_critical " -- connection failed rather than downgrading to $latest_supported_string"
+ fileout "$jsonID" "CRITICAL" "connection failed rather than downgrading to $latest_supported_string"
+ fi
+ ;;
+ 2) add_tls_offered tls1_2 no
+ pr_svrty_medium "not offered and downgraded to a weaker protocol"
+ if [[ "$tls12_detected_version" == 0300 ]]; then
+ detected_version_string="SSLv3"
+ elif [[ "$tls12_detected_version" == 03* ]]; then
+ detected_version_string="TLSv1.$((0x$tls12_detected_version-0x0301))"
+ fi
+ if [[ "$tls12_detected_version" == "$latest_supported" ]]; then
+ outln
+ fileout "$jsonID" "MEDIUM" "not offered and downgraded to a weaker protocol"
+ elif [[ "$tls12_detected_version" == 03* ]] && [[ 0x$tls12_detected_version -lt 0x$latest_supported ]]; then
+ prln_svrty_critical " -- server supports $latest_supported_string, but downgraded to $detected_version_string"
+ fileout "$jsonID" "CRITICAL" "not offered, and downgraded to $detected_version_string rather than $latest_supported_string"
+ elif [[ "$tls12_detected_version" == 03* ]] && [[ 0x$tls12_detected_version -gt 0x0303 ]]; then
+ prln_svrty_critical " -- server responded with higher version number ($detected_version_string) than requested by client"
+ fileout "$jsonID" "CRITICAL" "not offered, server responded with higher version number ($detected_version_string) than requested by client"
+ else
+ if [[ ${#tls12_detected_version} -eq 4 ]]; then
+ prln_svrty_critical "server responded with version number ${tls12_detected_version:0:2}.${tls12_detected_version:2:2} (NOT ok)"
+ fileout "$jsonID" "CRITICAL" "server responded with version number ${tls12_detected_version:0:2}.${tls12_detected_version:2:2}"
+ else
+ prln_svrty_medium " -- strange, server ${tls12_detected_version}"
+ fileout "$jsonID" "MEDIUM" "strange, server ${tls12_detected_version}"
+ fi
+ fi
+ ;;
+ 3) out "not offered, "
+ fileout "$jsonID" "INFO" "not offered"
+ add_tls_offered tls1_2 no
+ pr_warning "TLS downgraded to STARTTLS plaintext"; outln
+ fileout "$jsonID" "WARN" "TLS downgraded to STARTTLS plaintext"
+ ;;
+ 4) out "likely "; pr_svrty_medium "not offered, "
+ fileout "$jsonID" "MEDIUM" "not offered"
+ add_tls_offered tls1_2 no
+ pr_warning "received 4xx/5xx after STARTTLS handshake"; outln "$debug_recomm"
+ fileout "$jsonID" "WARN" "received 4xx/5xx after STARTTLS handshake${debug_recomm}"
+ ;;
+ 5) outln "$supported_no_ciph1" # protocol detected, but no cipher --> comes from run_prototest_openssl
+ fileout "$jsonID" "INFO" "$supported_no_ciph1"
+ add_tls_offered tls1_2 yes
+ ;;
+ 7) if "$using_sockets" ; then
+ # can only happen in debug mode
+ pr_warning "strange reply, maybe a client side problem with TLS 1.2"; outln "$debug_recomm"
+ else
+ prln_local_problem "$OPENSSL doesn't support \"s_client -tls1_2\""
+ fileout "$jsonID" "WARN" "not tested due to lack of local support"
+ fi
+ ((ret++))
+ ;;
+ *) pr_fixme "unexpected value around line $((LINENO))"; outln "$debug_recomm"
+ ((ret++))
+ ;;
+ esac
+
+ pr_bold " TLS 1.3 ";
+ jsonID="TLS1_3"
+ case $ret_val_tls13 in
+ 0) if ! "$using_sockets"; then
+ prln_svrty_best "offered (OK)"
+ fileout "$jsonID" "OK" "offered"
+ else
+ # If TLS 1.3 is offered, then its support was detected
+ # by determine_optimal_sockets_params().
+ if [[ $(has_server_protocol tls1_3_rfc8446) -eq 0 ]]; then
+ drafts_offered+=" 0304 "
+ else
+ for i in 1C 1B 1A 19 18 17 16 15 14 13 12; do
+ if [[ $(has_server_protocol tls1_3_draft$(hex2dec "$i")) -eq 0 ]]; then
+ drafts_offered+=" 7F$i "
+ break
+ fi
+ done
+ fi
+ KEY_SHARE_EXTN_NR="28"
+ while true; do
+ supported_versions=""
+ for i in 16 15 14 13 12; do
+ [[ "$drafts_offered" =~ \ 7F$i\ ]] || supported_versions+=",7f,$i"
+ done
+ [[ -z "$supported_versions" ]] && break
+ supported_versions="00, 2b, 00, $(printf "%02x" $((${#supported_versions}/3+1))), $(printf "%02x" $((${#supported_versions}/3))) $supported_versions"
+ tls_sockets "04" "$TLS13_CIPHER" "" "$supported_versions"
+ [[ $? -eq 0 ]] || break
+ if [[ "${TLS_SERVER_HELLO:8:3}" == 7F1 ]]; then
+ drafts_offered+=" ${TLS_SERVER_HELLO:8:4} "
+ elif [[ "$TLS_SERVER_HELLO" =~ 002B00027F1[2-6] ]]; then
+ drafts_offered+=" ${BASH_REMATCH:8:4} "
+ fi
+ done
+ KEY_SHARE_EXTN_NR="33"
+ while true; do
+ supported_versions=""
+ for i in 1C 1B 1A 19 18 17; do
+ [[ "$drafts_offered" =~ \ 7F$i\ ]] || supported_versions+=",7f,$i"
+ done
+ [[ "$drafts_offered" =~ \ 0304\ ]] || supported_versions+=",03,04"
+ [[ -z "$supported_versions" ]] && break
+ supported_versions="00, 2b, 00, $(printf "%02x" $((${#supported_versions}/3+1))), $(printf "%02x" $((${#supported_versions}/3))) $supported_versions"
+ tls_sockets "04" "$TLS13_CIPHER" "" "$supported_versions"
+ [[ $? -eq 0 ]] || break
+ if [[ "$TLS_SERVER_HELLO" =~ 002B00020304 ]]; then
+ drafts_offered+=" 0304 "
+ elif [[ "$TLS_SERVER_HELLO" =~ 002B00027F1[7-9A-C] ]]; then
+ drafts_offered+=" ${BASH_REMATCH:8:4} "
+ fi
+ done
+ KEY_SHARE_EXTN_NR="$key_share_extn_nr"
+ if [[ -n "$drafts_offered" ]]; then
+ for i in 1C 1B 1A 19 18 17 16 15 14 13 12; do
+ if [[ "$drafts_offered" =~ \ 7F$i\ ]]; then
+ [[ -n "$drafts_offered_str" ]] && drafts_offered_str+=", "
+ drafts_offered_str+="draft $(printf "%d" 0x$i)"
+ fi
+ done
+ if [[ "$drafts_offered" =~ \ 0304\ ]]; then
+ [[ -n "$drafts_offered_str" ]] && drafts_offered_str+=", "
+ drafts_offered_str+="final"
+ fi
+ if [[ "$drafts_offered" =~ \ 0304\ ]]; then
+ pr_svrty_best "offered (OK)"; outln ": $drafts_offered_str"
+ fileout "$jsonID" "OK" "offered with $drafts_offered_str"
+ else
+ out "offered (OK)"; outln ": $drafts_offered_str"
+ fileout "$jsonID" "INFO" "offered with $drafts_offered_str"
+ fi
+ else
+ pr_warning "Unexpected results"; outln "$debug_recomm"
+ fileout "$jsonID" "WARN" "unexpected results"
+ fi
+ fi
+ latest_supported="0304"
+ latest_supported_string="TLSv1.3"
+ add_tls_offered tls1_3 yes
+ ;;
+ 1) pr_svrty_low "not offered"
+ if [[ -z $latest_supported ]]; then
+ outln
+ fileout "$jsonID" "LOW" "not offered"
+ else
+ prln_svrty_critical " -- connection failed rather than downgrading to $latest_supported_string"
+ fileout "$jsonID" "CRITICAL" "connection failed rather than downgrading to $latest_supported_string"
+ fi
+ add_tls_offered tls1_3 no
+ ;;
+ 2) if [[ "$DETECTED_TLS_VERSION" == 0300 ]]; then
+ detected_version_string="SSLv3"
+ elif [[ "$DETECTED_TLS_VERSION" == 03* ]]; then
+ detected_version_string="TLSv1.$((0x$DETECTED_TLS_VERSION-0x0301))"
+ fi
+ if [[ "$DETECTED_TLS_VERSION" == "$latest_supported" ]]; then
+ outln "not offered and downgraded to a weaker protocol"
+ fileout "$jsonID" "INFO" "not offered + downgraded to weaker protocol"
+ elif [[ "$DETECTED_TLS_VERSION" == 03* ]] && [[ 0x$DETECTED_TLS_VERSION -lt 0x$latest_supported ]]; then
+ out "not offered"
+ prln_svrty_critical " -- server supports $latest_supported_string, but downgraded to $detected_version_string"
+ fileout "$jsonID" "CRITICAL" "not offered, and downgraded to $detected_version_string rather than $latest_supported_string"
+ elif [[ "$DETECTED_TLS_VERSION" == 03* ]] && [[ 0x$DETECTED_TLS_VERSION -gt 0x0304 ]]; then
+ out "not offered"
+ prln_svrty_critical " -- server responded with higher version number ($detected_version_string) than requested by client"
+ fileout "$jsonID" "CRITICAL" "not offered, server responded with higher version number ($detected_version_string) than requested by client"
+ else
+ out "not offered"
+ prln_svrty_critical " -- server responded with version number ${DETECTED_TLS_VERSION:0:2}.${DETECTED_TLS_VERSION:2:2}"
+ fileout "$jsonID" "CRITICAL" "server responded with version number ${DETECTED_TLS_VERSION:0:2}.${DETECTED_TLS_VERSION:2:2}"
+ fi
+ add_tls_offered tls1_3 no
+ ;;
+ 3) out "not offered "
+ fileout "$jsonID" "INFO" "not offered"
+ add_tls_offered tls1_3 no
+ pr_warning "TLS downgraded to STARTTLS plaintext"; outln
+ fileout "$jsonID" "WARN" "TLS downgraded to STARTTLS plaintext"
+ ;;
+ 4) out "likely not offered, "
+ fileout "$jsonID" "INFO" "not offered"
+ add_tls_offered tls1_3 no
+ pr_warning "received 4xx/5xx after STARTTLS handshake"; outln "$debug_recomm"
+ fileout "$jsonID" "WARN" "received 4xx/5xx after STARTTLS handshake${debug_recomm}"
+ ;;
+ 5) outln "$supported_no_ciph1" # protocol detected but no cipher --> comes from run_prototest_openssl
+ fileout "$jsonID" "INFO" "$supported_no_ciph1"
+ add_tls_offered tls1_3 yes
+ ;;
+ 7) if "$using_sockets" ; then
+ # can only happen in debug mode
+ prln_warning "strange reply, maybe a client side problem with TLS 1.3"; outln "$debug_recomm"
+ else
+ prln_local_problem "$OPENSSL doesn't support \"s_client -tls1_3\""
+ fileout "$jsonID" "WARN" "not tested due to lack of local support"
+ fi
+ ((ret++))
+ ;;
+ *) pr_fixme "unexpected value around line $((LINENO))"; outln "$debug_recomm"
+ ((ret++))
+ ;;
+ esac
+
+ debugme echo "PROTOS_OFFERED: $PROTOS_OFFERED"
+ if [[ ! "$PROTOS_OFFERED" =~ yes ]]; then
+ outln
+ ignore_no_or_lame "You should not proceed as no protocol was detected. If you still really really want to, say \"YES\"" "YES"
+ [[ $? -ne 0 ]] && exit $ERR_CLUELESS
+ fi
+
+ return $ret
+}
+
+
+# list ciphers (and makes sure you have them locally configured)
+# arg[1]: non-TLSv1.3 cipher list (or anything else)
+# arg[2]: TLSv1.3 cipher list
+# arg[3]: protocol (e.g., -ssl2)
+#
+listciphers() {
+ local -i ret
+ local debugname=""
+ local tls13_ciphers="$TLS13_OSSL_CIPHERS"
+
+ [[ "$2" != ALL ]] && tls13_ciphers="$2"
+ if "$HAS_CIPHERSUITES"; then
+ $OPENSSL ciphers $OSSL_CIPHERS_S $3 -ciphersuites "$tls13_ciphers" "$1" &>$TMPFILE
+ elif [[ -n "$tls13_ciphers" ]]; then
+ $OPENSSL ciphers $OSSL_CIPHERS_S $3 "$tls13_ciphers:$1" &>$TMPFILE
+ else
+ $OPENSSL ciphers $OSSL_CIPHERS_S $3 "$1" &>$TMPFILE
+ fi
+ ret=$?
+ debugme cat $TMPFILE
+ debugname="$(sed -e s'/\!/not/g' -e 's/\:/_/g' <<< "$1")"
+ tmpfile_handle ${FUNCNAME[0]}.${debugname}.txt
+ return $ret
+}
+
+
+# argv[1]: non-TLSv1.3 cipher list to test in OpenSSL syntax
+# argv[2]: TLSv1.3 cipher list to test in OpenSSL syntax
+# argv[3]: string on console / HTML or "finding"
+# argv[4]: rating whether ok to offer
+# argv[5]: string to be appended for fileout
+# argv[6]: non-SSLv2 cipher list to test (hexcodes), if using sockets
+# argv[7]: SSLv2 cipher list to test (hexcodes), if using sockets
+# argv[8]: true if using sockets, false if not
+# argv[9]: CVE
+# argv[10]: CWE
+#
+sub_cipherlists() {
+ local -i i len sclient_success=1
+ local cipherlist sslv2_cipherlist detected_ssl2_ciphers
+ local singlespaces
+ local proto=""
+ local -i ret=0
+ local jsonID="cipherlist"
+ local using_sockets="${8}"
+ local cve="${9}"
+ local cwe="${10}"
+
+ pr_bold "$3 "
+ [[ "$OPTIMAL_PROTO" == -ssl2 ]] && proto="$OPTIMAL_PROTO"
+ jsonID="${jsonID}_$5"
+
+ if "$using_sockets" || listciphers "$1" "$2" $proto; then
+ if ! "$using_sockets" || ( "$FAST" && listciphers "$1" "$2" -tls1 ); then
+ for proto in -no_ssl2 -tls1_2 -tls1_1 -tls1 -ssl3; do
+ if [[ "$proto" == -tls1_2 ]]; then
+ # If $OPENSSL doesn't support TLSv1.3 or if no TLSv1.3
+ # ciphers are being tested, then a TLSv1.2 ClientHello
+ # was tested in the first iteration.
+ ! "$HAS_TLS13" && continue
+ [[ -z "$2" ]] && continue
+ fi
+ ! "$HAS_SSL3" && [[ "$proto" == -ssl3 ]] && continue
+ if [[ "$proto" != -no_ssl2 ]]; then
+ "$FAST" && continue
+ [[ $(has_server_protocol "${proto:1}") -eq 1 ]] && continue
+ fi
+ $OPENSSL s_client $(s_client_options "-cipher "$1" -ciphersuites "\'$2\'" $BUGS $STARTTLS -connect $NODEIP:$PORT $PROXY $SNI $proto") 2>$ERRFILE >$TMPFILE </dev/null
+ sclient_connect_successful $? $TMPFILE
+ sclient_success=$?
+ debugme cat $ERRFILE
+ [[ $sclient_success -eq 0 ]] && break
+ done
+ else
+ for proto in 04 03 02 01 00; do
+ # If $cipherlist doesn't contain any TLSv1.3 ciphers, then there is
+ # no reason to try a TLSv1.3 ClientHello.
+ [[ "$proto" == 04 ]] && [[ ! "$6" =~ 13,0 ]] && continue
+ [[ $(has_server_protocol "$proto") -eq 1 ]] && continue
+ cipherlist="$(strip_inconsistent_ciphers "$proto" ", $6")"
+ cipherlist="${cipherlist:2}"
+ if [[ -n "$cipherlist" ]] && [[ "$cipherlist" != 00,ff ]]; then
+ tls_sockets "$proto" "$cipherlist"
+ sclient_success=$?
+ [[ $sclient_success -eq 2 ]] && sclient_success=0
+ [[ $sclient_success -eq 0 ]] && break
+ fi
+ done
+ fi
+ if [[ $sclient_success -ne 0 ]] && [[ 1 -ne $(has_server_protocol ssl2) ]]; then
+ if ( [[ -z "$7" ]] || "$FAST" ) && "$HAS_SSL2" && listciphers "$1" "" -ssl2; then
+ $OPENSSL s_client -cipher "$1" $BUGS $STARTTLS -connect $NODEIP:$PORT $PROXY -ssl2 2>$ERRFILE >$TMPFILE </dev/null
+ sclient_connect_successful $? $TMPFILE
+ sclient_success=$?
+ debugme cat $ERRFILE
+ elif [[ -n "$7" ]]; then
+ sslv2_sockets "$7" "true"
+ if [[ $? -eq 3 ]] && [[ "$V2_HELLO_CIPHERSPEC_LENGTH" -ne 0 ]]; then
+ sslv2_cipherlist="$(strip_spaces "${6//,/}")"
+ len=${#sslv2_cipherlist}
+ detected_ssl2_ciphers="$(grep "Supported cipher: " "$TEMPDIR/$NODEIP.parse_sslv2_serverhello.txt")"
+ for (( i=0; i<len; i=i+6 )); do
+ [[ "$detected_ssl2_ciphers" =~ "x${sslv2_cipherlist:i:6}" ]] && sclient_success=0 && break
+ done
+ fi
+ fi
+ fi
+ if [[ $sclient_success -ne 0 ]] && $BAD_SERVER_HELLO_CIPHER; then
+ # If server failed with a known error, raise it to the user.
+ if [[ $STARTTLS_PROTOCOL == mysql ]]; then
+ pr_warning "SERVER_ERROR: test inconclusive due to MySQL Community Edition (yaSSL) bug."
+ fileout "$jsonID" "WARN" "SERVER_ERROR, test inconclusive due to MySQL Community Edition (yaSSL) bug." "$cve" "$cwe"
+ else
+ pr_warning "SERVER_ERROR: test inconclusive."
+ fileout "$jsonID" "WARN" "SERVER_ERROR, test inconclusive." "$cve" "$cwe"
+ fi
+ ((ret++))
+ else
+ # Otherwise the error means the server doesn't support that cipher list.
+ case $4 in
+ 7) if [[ $sclient_success -eq 0 ]]; then
+ # Strong is excellent to offer
+ pr_svrty_best "offered (OK)"
+ fileout "$jsonID" "OK" "offered" "$cve" "$cwe"
+ else
+ pr_svrty_medium "not offered"
+ fileout "$jsonID" "MEDIUM" "not offered" "$cve" "$cwe"
+ fi
+ ;;
+ 6) if [[ $sclient_success -eq 0 ]]; then
+ # High is good to offer
+ pr_svrty_good "offered (OK)"
+ fileout "$jsonID" "OK" "offered" "$cve" "$cwe"
+ else
+ # FIXME: we penalize the absence of high but don't know the result of strong encryption yet (next)
+ pr_svrty_medium "not offered"
+ fileout "$jsonID" "MEDIUM" "not offered" "$cve" "$cwe"
+ fi
+ ;;
+ 5) if [[ $sclient_success -eq 0 ]]; then
+ # Neither good nor bad to offer
+ out "offered (OK)"
+ fileout "$jsonID" "INFO" "offered" "$cve" "$cwe"
+ else
+ out "not offered"
+ fileout "$jsonID" "INFO" "not offered" "$cve" "$cwe"
+ fi
+ ;;
+ 4) if [[ $sclient_success -eq 0 ]]; then
+ # medium is not that bad
+ pr_svrty_low "offered"
+ fileout "$jsonID" "LOW" "offered" "$cve" "$cwe"
+ else
+ out "not offered"
+ fileout "$jsonID" "INFO" "not offered" "$cve" "$cwe"
+ fi
+ ;;
+ 3) if [[ $sclient_success -eq 0 ]]; then
+ pr_svrty_medium "offered"
+ fileout "$jsonID" "MEDIUM" "offered" "$cve" "$cwe"
+ else
+ out "not offered"
+ fileout "$jsonID" "INFO" "not offered" "$cve" "$cwe"
+ fi
+ ;;
+ 2) if [[ $sclient_success -eq 0 ]]; then
+ # bad but there is worse
+ pr_svrty_high "offered (NOT ok)"
+ fileout "$jsonID" "HIGH" "offered" "$cve" "$cwe"
+ else
+ # need a check for -eq 1 here
+ pr_svrty_good "not offered (OK)"
+ fileout "$jsonID" "OK" "not offered" "$cve" "$cwe"
+ fi
+ ;;
+ 1) if [[ $sclient_success -eq 0 ]]; then
+ # the ugly ones
+ pr_svrty_critical "offered (NOT ok)"
+ fileout "$jsonID" "CRITICAL" "offered" "$cve" "$cwe"
+ else
+ pr_svrty_best "not offered (OK)"
+ fileout "$jsonID" "OK" "not offered" "$cve" "$cwe"
+ fi
+ ;;
+ *) # we shouldn't reach this
+ pr_warning "?: $4 (please report this)"
+ fileout "$jsonID" "WARN" "return condition $4 unclear" "$cve" "$cwe"
+ ((ret++))
+ ;;
+ esac
+ fi
+ tmpfile_handle ${FUNCNAME[0]}.${5}.txt
+ [[ $DEBUG -ge 1 ]] && tm_out " -- $1"
+ outln
+ else
+ singlespaces=$(sed -e 's/ \+/ /g' -e 's/^ //' -e 's/ $//g' -e 's/ //g' <<< "$3")
+ if [[ "$OPTIMAL_PROTO" == -ssl2 ]]; then
+ prln_local_problem "No $singlespaces for SSLv2 configured in $OPENSSL"
+ else
+ prln_local_problem "No $singlespaces configured in $OPENSSL"
+ fi
+ fileout "$jsonID" "WARN" "Cipher $3 ($1) not supported by local OpenSSL ($OPENSSL)"
+ fi
+ return $ret
+}
+
+#TODO: work with fixed lists here --> atm ok, as sockets are preferred. If there would be a single function for testing: yes.
+run_cipherlists() {
+ local hexc hexcode strength
+ local -i i
+ local -i ret=0
+ local ossl_null_ciphers null_ciphers sslv2_null_ciphers
+ local ossl_anon_ciphers anon_ciphers sslv2_anon_ciphers
+ local ossl_exp_ciphers exp_ciphers sslv2_exp_ciphers
+ local ossl_low_ciphers low_ciphers sslv2_low_ciphers
+ local ossl_tdes_ciphers tdes_ciphers sslv2_tdes_cipher
+ local ossl_average_ciphers average_ciphers
+ local strong_ciphers
+ local cwe="CWE-327"
+ local cwe2="CWE-310"
+ local cve=""
+ local using_sockets=true
+
+ outln
+ pr_headlineln " Testing cipher categories "
+ outln
+ "$SSL_NATIVE" && using_sockets=false
+
+ # conversion 2 byte ciphers via: echo "$@" | sed -e 's/[[:xdigit:]]\{2\},/0x&/g' -e 's/, /\n/g' | while read ci; do grep -wi $ci etc/cipher-mapping.txt; done
+
+ ossl_null_ciphers='NULL:eNULL'
+ null_ciphers="c0,10, c0,06, c0,15, c0,0b, c0,01, c0,3b, c0,3a, c0,39, 00,b9, 00,b8, 00,b5, 00,b4, 00,2e, 00,2d, 00,b1, 00,b0, 00,2c, 00,3b, 00,02, 00,01, 00,82, 00,83, ff,87, 00,ff"
+ sslv2_null_ciphers="FF,80,10, 00,00,00"
+
+ ossl_anon_ciphers='aNULL:ADH'
+ anon_ciphers="c0,19, 00,a7, 00,6d, 00,3a, 00,c5, 00,89, c0,47, c0,5b, c0,85, c0,18, 00,a6, 00,6c, 00,34, 00,bf, 00,9b, 00,46, c0,46, c0,5a, c0,84, c0,16, 00,18, c0,17, 00,1b, 00,1a, 00,19, 00,17, c0,15, 00,ff"
+ sslv2_anon_ciphers="FF,80,10"
+
+ ossl_exp_ciphers='EXPORT:!ADH:!NULL'
+ # grep -i EXP etc/cipher-mapping.txt
+ exp_ciphers="00,63, 00,62, 00,61, 00,65, 00,64, 00,60, 00,14, 00,11, 00,19, 00,08, 00,06, 00,27, 00,26, 00,2a, 00,29, 00,0b, 00,0e, 00,17, 00,03, 00,28, 00,2b, 00,ff"
+ sslv2_exp_ciphers="04,00,80, 02,00,80, 00,00,00"
+
+ ossl_low_ciphers='LOW:DES:RC2:RC4:!ADH:!EXP:!NULL:!eNULL'
+ # egrep -w '64|56|RC2|RC4' etc/cipher-mapping.txt | egrep -v 'Au=None|export'
+ low_ciphers="00,04, 00,05, 00,09, 00,0C, 00,0F, 00,12, 00,15, 00,1E, 00,20, 00,22, 00,24, 00,66, 00,8A, 00,8E, 00,92, C0,02, C0,07, C0,0C, C0,11, C0,33, FE,FE, FF,E1, 00,FF"
+ sslv2_low_ciphers="01,00,80, 03,00,80, 06,00,40, 06,01,40, 08,00,80, FF,80,00"
+
+ ossl_tdes_ciphers='3DES:IDEA:!aNULL:!ADH'
+ # egrep -w '3DES|IDEA' etc/cipher-mapping.txt | grep -v "Au=None"
+ tdes_ciphers="00,07, 00,0A, 00,0D, 00,10, 00,13, 00,16, 00,1F, 00,21, 00,23, 00,25, 00,8B, 00,8F, 00,93, C0,03, C0,08, C0,0D, C0,12, C0,1A, C0,1B, C0,1C, C0,34, FE,FF, FF,E0, 00,FF"
+ sslv2_tdes_ciphers="05,00,80, 07,00,c0, 07,01,c0"
+
+ # Now all AES, CAMELLIA, ARIA and SEED CBC ciphers plus GOST
+ ossl_average_ciphers='HIGH:MEDIUM:AES:CAMELLIA:ARIA:!IDEA:!CHACHA20:!3DES:!RC2:!RC4:!AESCCM8:!AESCCM:!AESGCM:!ARIAGCM:!aNULL'
+ # egrep -w "256|128" etc/cipher-mapping.txt | egrep -v "Au=None|AEAD|RC2|RC4|IDEA"
+ average_ciphers="00,2F, 00,30, 00,31, 00,32, 00,33, 00,35, 00,36, 00,37, 00,38, 00,39, 00,3C, 00,3D, 00,3E, 00,3F, 00,40, 00,41, 00,42, 00,43, 00,44, 00,45, 00,67, 00,68, 00,69, 00,6A, 00,6B, 00,84, 00,85, 00,86, 00,87, 00,88, 00,8C, 00,8D, 00,90, 00,91, 00,94, 00,95, 00,96, 00,97, 00,98, 00,99, 00,9A, 00,AE, 00,AF, 00,B2, 00,B3, 00,B6, 00,B7, 00,BA, 00,BB, 00,BC, 00,BD, 00,BE, 00,C0, 00,C1, 00,C2, 00,C3, 00,C4, C0,04, C0,05, C0,09, C0,0A, C0,0E, C0,0F, C0,13, C0,14, C0,1D, C0,1E, C0,1F, C0,20, C0,21, C0,22, C0,23, C0,24, C0,25, C0,26, C0,27, C0,28, C0,29, C0,2A, C0,35, C0,36, C0,37, C0,38, C0,3C, C0,3D, C0,3E, C0,3F, C0,40, C0,41, C0,42, C0,43, C0,44, C0,45, C0,48, C0,49, C0,4A, C0,4B, C0,4C, C0,4D, C0,4E, C0,4F, C0,64, C0,65, C0,66, C0,67, C0,68, C0,69, C0,70, C0,71, C0,72, C0,73, C0,74, C0,75, C0,76, C0,77, C0,78, C0,79, C0,94, C0,95, C0,96, C0,97, C0,98, C0,99, C0,9A, C0,9B"
+ # Workaround: If we use sockets and in order not to hit 132+1 ciphers we omit the GOST ciphers if SERVER_SIZE_LIMIT_BUG is true.
+ # This won't be supported by Cisco ACE anyway. Catch is, if SERVER_SIZE_LIMIT_BUG was not tested for before (only this function is being called)
+ "$SERVER_SIZE_LIMIT_BUG" || average_ciphers="${average_ciphers}, 00,80, 00,81, FF,00, FF,01, FF,02, FF,03, FF,85"
+ average_ciphers="${average_ciphers}, 00,FF"
+
+ # Here's the strongest discrepancy between sockets and OpenSSL
+ ossl_strong_ciphers='AESGCM:CHACHA20:AESGCM:CamelliaGCM:AESCCM:ARIAGCM'
+ # grep AEAD etc/cipher-mapping.txt | grep -v Au=None
+ strong_ciphers="00,9C, 00,9D, 00,9E, 00,9F, 00,A0, 00,A1, 00,A2, 00,A3, 00,A4, 00,A5, 00,A8, 00,A9, 00,AA, 00,AB, 00,AC, 00,AD, 13,01, 13,02, 13,03, 13,04, 13,05, 16,B7, 16,B8, 16,B9, 16,BA, C0,2B, C0,2C, C0,2D, C0,2E, C0,2F, C0,30, C0,31, C0,32, C0,50, C0,51, C0,52, C0,53, C0,54, C0,55, C0,56, C0,57, C0,58, C0,59, C0,5C, C0,5D, C0,5E, C0,5F, C0,60, C0,61, C0,62, C0,63, C0,6A, C0,6B, C0,6C, C0,6D, C0,6E, C0,6F, C0,7A, C0,7B, C0,7C, C0,7D, C0,7E, C0,7F, C0,80, C0,81, C0,82, C0,83, C0,86, C0,87, C0,88, C0,89, C0,8A, C0,8B, C0,8C, C0,8D, C0,8E, C0,8F, C0,90, C0,91, C0,92, C0,93, C0,9C, C0,9D, C0,9E, C0,9F, C0,A0, C0,A1, C0,A2, C0,A3, C0,A4, C0,A5, C0,A6, C0,A7, C0,A8, C0,A9, C0,AA, C0,AB, C0,AC, C0,AD, C0,AE, C0,AF, CC,13, CC,14, CC,15, CC,A8, CC,A9, CC,AA, CC,AB, CC,AC, CC,AD, CC,AE, 00,FF"
+
+ # argv[1]: non-TLSv1.3 cipher list to test in OpenSSL syntax
+ # argv[2]: TLSv1.3 cipher list to test in OpenSSL syntax
+ # argv[3]: string on console / HTML or "finding"
+ # argv[4]: rating whether ok to offer
+ # argv[5]: string to be appended for fileout
+ # argv[6]: non-SSLv2 cipher list to test (hexcodes), if using sockets
+ # argv[7]: SSLv2 cipher list to test (hexcodes), if using sockets
+ # argv[8]: true if using sockets, false if not
+ # argv[9]: CVE
+ # argv[10]: CWE
+
+ sub_cipherlists "$ossl_null_ciphers" "" " NULL ciphers (no encryption) " 1 "NULL" "$null_ciphers" "$sslv2_null_ciphers" "$using_sockets" "$cve" "$cwe"
+ ret=$?
+ sub_cipherlists "$ossl_anon_ciphers" "" " Anonymous NULL Ciphers (no authentication)" 1 "aNULL" "$anon_ciphers" "$sslv2_anon_ciphers" "$using_sockets" "$cve" "$cwe"
+ ret=$((ret + $?))
+ sub_cipherlists "$ossl_exp_ciphers" "" " Export ciphers (w/o ADH+NULL) " 1 "EXPORT" "$exp_ciphers" "$sslv2_exp_ciphers" "$using_sockets" "$cve" "$cwe"
+ ret=$((ret + $?))
+ sub_cipherlists "$ossl_low_ciphers" "" " LOW: 64 Bit + DES, RC[2,4] (w/o export) " 2 "LOW" "$low_ciphers" "$sslv2_low_ciphers" "$using_sockets" "$cve" "$cwe"
+ ret=$((ret + $?))
+ sub_cipherlists "$ossl_tdes_ciphers" "" " Triple DES Ciphers / IDEA " 3 "3DES_IDEA" "$tdes_ciphers" "$sslv2_tdes_ciphers" "$using_sockets" "$cve" "$cwe2"
+ ret=$((ret + $?))
+ sub_cipherlists "$ossl_average_ciphers" "" " Obsolete CBC ciphers (AES, ARIA etc.) " 4 "AVERAGE" "$average_ciphers" "" "$using_sockets" "$cve" "$cwe2"
+ ret=$((ret + $?))
+ sub_cipherlists "$ossl_strong_ciphers" 'ALL' " Strong encryption (AEAD ciphers) " 7 "STRONG" "$strong_ciphers" "" "$using_sockets" "" ""
+ ret=$((ret + $?))
+
+ outln
+ return $ret
+}
+
+# The return value is an indicator of the quality of the DH key length in $1:
+# 1 = pr_svrty_critical, 2 = pr_svrty_high, 3 = pr_svrty_medium, 4 = pr_svrty_low
+# 5 = neither good nor bad, 6 = pr_svrty_good, 7 = pr_svrty_best
+pr_dh_quality() {
+ local bits="$1"
+ local string="$2"
+
+ if [[ "$bits" -le 600 ]]; then
+ pr_svrty_critical "$string"
+ return 1
+ elif [[ "$bits" -le 800 ]]; then
+ pr_svrty_high "$string"
+ return 2
+ elif [[ "$bits" -le 1280 ]]; then
+ pr_svrty_medium "$string"
+ return 3
+ elif [[ "$bits" -ge 2048 ]]; then
+ pr_svrty_good "$string"
+ return 6
+ else
+ out "$string"
+ return 5
+ fi
+}
+
+# prints out dh group=prime and in round brackets DH bits and labels it accordingly
+# arg1: name of dh group, arg2=bit length
+pr_dh() {
+ local -i quality=0
+
+ pr_italic "$1"
+ out " ("
+ pr_dh_quality "$2" "$2 bits"
+ quality=$?
+ out ")"
+ return $quality
+}
+
+pr_ecdh_quality() {
+ local bits="$1"
+ local string="$2"
+
+ if [[ "$bits" -le 80 ]]; then # has that ever existed?
+ pr_svrty_critical "$string"
+ elif [[ "$bits" -le 108 ]]; then # has that ever existed?
+ pr_svrty_high "$string"
+ elif [[ "$bits" -le 163 ]]; then
+ pr_svrty_medium "$string"
+ elif [[ "$bits" -le 193 ]]; then # hmm, according to https://wiki.openssl.org/index.php/Elliptic_Curve_Cryptography it should ok
+ pr_svrty_low "$string" # but openssl removed it https://github.com/drwetter/testssl.sh/issues/299#issuecomment-220905416
+ elif [[ "$bits" -le 224 ]]; then
+ out "$string"
+ elif [[ "$bits" -gt 224 ]]; then
+ pr_svrty_good "$string"
+ else
+ out "$string"
+ fi
+}
+
+pr_ecdh_curve_quality() {
+ curve="$1"
+ local -i bits=0
+
+ case "$curve" in
+ "sect163k1") bits=163 ;;
+ "sect163r1") bits=162 ;;
+ "sect163r2") bits=163 ;;
+ "sect193r1") bits=193 ;;
+ "sect193r2") bits=193 ;;
+ "sect233k1") bits=232 ;;
+ "sect233r1") bits=233 ;;
+ "sect239k1") bits=238 ;;
+ "sect283k1") bits=281 ;;
+ "sect283r1") bits=282 ;;
+ "sect409k1") bits=407 ;;
+ "sect409r1") bits=409 ;;
+ "sect571k1") bits=570 ;;
+ "sect571r1") bits=570 ;;
+ "secp160k1") bits=161 ;;
+ "secp160r1") bits=161 ;;
+ "secp160r2") bits=161 ;;
+ "secp192k1") bits=192 ;;
+ "prime192v1") bits=192 ;;
+ "secp224k1") bits=225 ;;
+ "secp224r1") bits=224 ;;
+ "secp256k1") bits=256 ;;
+ "prime256v1") bits=256 ;;
+ "secp384r1") bits=384 ;;
+ "secp521r1") bits=521 ;;
+ "brainpoolP256r1") bits=256 ;;
+ "brainpoolP384r1") bits=384 ;;
+ "brainpoolP512r1") bits=512 ;;
+ "X25519") bits=253 ;;
+ "X448") bits=448 ;;
+ esac
+ pr_ecdh_quality "$bits" "$curve"
+}
+
+# Print $2 based on the quality of the cipher in $1. If $2 is empty, just print $1.
+# The return value is an indicator of the quality of the cipher in $1:
+# 0 = $1 is empty
+# 1 = pr_svrty_critical, 2 = pr_svrty_high, 3 = pr_svrty_medium, 4 = pr_svrty_low
+# 5 = neither good nor bad, 6 = pr_svrty_good, 7 = pr_svrty_best
+#
+# Please note this section isn't particular spot on. It needs to be reconsidered/redone
+# SHA1, SSLv3 ciphers are some points which need to be considered.
+# Hint: find out by "grep <pattern> etc/cipher-mapping.txt" but it' might be be easier
+# to look out Enc= and Au= or Mac=
+#
+pr_cipher_quality() {
+ local cipher="$1"
+ local text="$2"
+
+ [[ -z "$1" ]] && return 0
+ [[ -z "$text" ]] && text="$cipher"
+
+ if [[ "$cipher" != TLS_* ]] && [[ "$cipher" != SSL_* ]]; then
+ # This must be the OpenSSL name for a cipher or for TLS 1.3 ($TLS13_OSSL_CIPHERS)
+ # We can ignore them however as the OpenSSL and RFC names currently match
+ if [[ $TLS_NR_CIPHERS -eq 0 ]]; then
+ # We have an OpenSSL name and can't convert it to the RFC name which is rarely
+ # the case, see "prepare_arrays()" and "./etc/cipher-mapping.txt"
+ case "$cipher" in
+ *NULL*|EXP*|ADH*)
+ pr_svrty_critical "$text"
+ return 1
+ ;;
+ *RC4*|*RC2*|*MD5|*M1)
+ pr_svrty_high "$text"
+ return 2
+ ;;
+ AES256-GCM-SHA384|AES128-GCM-SHA256|AES256-CCM|AES128-CCM|ARIA256-GCM-SHA384|ARIA128-GCM-SHA256)
+ # RSA kx and e.g. GCM isn't certainly the best
+ pr_svrty_good "$text"
+ return 6
+ ;;
+ *GCM*|*CCM*|*CHACHA20*)
+ pr_svrty_best "$text"
+ return 7
+ ;; #best ones
+ *CBC3*|*SEED*|*3DES*|*IDEA*)
+ pr_svrty_medium "$text"
+ return 3
+ ;;
+ ECDHE*AES*|DHE*AES*SHA*|*CAMELLIA*SHA)
+ pr_svrty_low "$text"
+ return 4
+ ;;
+ *)
+ out "$text"
+ return 5
+ ;;
+ esac
+ fi
+ cipher="$(openssl2rfc "$cipher")"
+ fi
+
+ # Now we look at the RFC cipher names. The sequence matters - as above.
+ case "$cipher" in
+ *NULL*|*EXP*|*_DES40_*|*anon*)
+ pr_svrty_critical "$text"
+ return 1
+ ;;
+ *RC4*|*RC2*|*MD5|*MD5_1)
+ pr_svrty_high "$text"
+ return 2
+ ;;
+ *_DES_*)
+ if [[ "$cipher" =~ EDE3 ]]; then
+ pr_svrty_medium "$text" # 3DES
+ return 3
+ fi
+ pr_svrty_high "$text"
+ return 2
+ ;;
+ *CBC3*|*SEED*|*3DES*|*IDEA*)
+ pr_svrty_medium "$text"
+ return 3
+ ;;
+ TLS_RSA_*)
+ if [[ "$cipher" =~ CBC ]]; then
+ pr_svrty_low "$text"
+ return 4
+ else
+ pr_svrty_good "$text"
+ # RSA kx and e.g. GCM isn't certainly the best
+ return 6
+ fi
+ ;;
+ *GCM*|*CCM*|*CHACHA20*)
+ pr_svrty_best "$text"
+ return 7
+ ;;
+ *ECDHE*AES*CBC*|*DHE*AES*SHA*|*RSA*AES*SHA*|*CAMELLIA*SHA*)
+ pr_svrty_low "$text"
+ return 4
+ ;;
+ *)
+ out "$text"
+ return 5
+ ;;
+ esac
+}
+
+# arg1: file with input for grepping the type of ephemeral DH key (DH ECDH)
+read_dhtype_from_file() {
+ local temp kx
+
+ temp=$(awk -F': ' '/^Server Temp Key/ { print $2 }' "$1") # extract line
+ kx="Kx=${temp%%,*}"
+ [[ "$kx" == "Kx=X25519" ]] && kx="Kx=ECDH"
+ [[ "$kx" == "Kx=X448" ]] && kx="Kx=ECDH"
+ tm_out "$kx"
+ return 0
+}
+
+# arg1: certificate file
+read_sigalg_from_file() {
+ $OPENSSL x509 -noout -text -in "$1" 2>/dev/null | awk -F':' '/Signature Algorithm/ { print $2; exit; }'
+}
+
+
+# arg1: file with input for grepping the bit length for ECDH/DHE
+# arg2: whether to print warning "old fart" or not (empty: no)
+read_dhbits_from_file() {
+ local bits what_dh temp curve=""
+ local add=""
+ local old_fart=" (your $OPENSSL cannot show DH bits)"
+
+ temp=$(awk -F': ' '/^Server Temp Key/ { print $2 }' "$1") # extract line
+ what_dh="${temp%%,*}"
+ bits="${temp##*, }"
+ curve="${temp#*, }"
+ if [[ "$curve" == "$bits" ]]; then
+ curve=""
+ else
+ curve="${curve%%,*}"
+ fi
+ bits="${bits/bits/}"
+ bits="${bits// /}"
+
+ if [[ "$what_dh" == X25519 ]] || [[ "$what_dh" == X448 ]]; then
+ curve="$what_dh"
+ what_dh="ECDH"
+ fi
+ if [[ -z "$2" ]]; then
+ if [[ -n "$curve" ]]; then
+ debugme echo ">$HAS_DH_BITS|$what_dh($curve)|$bits<"
+ else
+ debugme echo ">$HAS_DH_BITS|$what_dh|$bits<"
+ fi
+ fi
+ [[ -n "$what_dh" ]] && HAS_DH_BITS=true # FIX 190
+ if [[ -z "$what_dh" ]] && ! "$HAS_DH_BITS"; then
+ if [[ "$2" == "string" ]]; then
+ tm_out "$old_fart"
+ elif [[ -z "$2" ]]; then
+ pr_warning "$old_fart"
+ fi
+ return 0
+ fi
+ if [[ "$2" == quiet ]]; then
+ tm_out "$bits"
+ return 0
+ fi
+ [[ -z "$2" ]] && [[ -n "$bits" ]] && out ", "
+ if [[ $what_dh == DH ]] || [[ $what_dh == EDH ]]; then
+ add="bit DH"
+ [[ -n "$curve" ]] && add+=" ($curve)"
+ if [[ "$2" == string ]]; then
+ tm_out ", $bits $add"
+ else
+ pr_dh_quality "$bits" "$bits $add"
+ fi
+ # https://wiki.openssl.org/index.php/Elliptic_Curve_Cryptography, https://www.keylength.com/en/compare/
+ elif [[ $what_dh == ECDH ]]; then
+ add="bit ECDH"
+ [[ -n "$curve" ]] && add+=" ($curve)"
+ if [[ "$2" == string ]]; then
+ tm_out ", $bits $add"
+ else
+ pr_ecdh_quality "$bits" "$bits $add"
+ fi
+ fi
+ return 0
+}
+
+
+# arg1: ID or empty. If empty resumption by ticket will be tested, otherwise by ID
+# return: 0: it has resumption, 1:nope, 2: nope (OpenSSL 1.1.1), 6: CLIENT_AUTH --> problem for resumption, 7: can't tell
+#
+# This is basically a short(?) version from Bulletproof SSL and TLS (p386). The version according to that would be e.g.
+# echo | $OPENSSL s_client -connect testssl.sh:443 -servername testssl.sh -no_ssl2 -reconnect 2>&1 | grep -E 'New|Reused'
+# echo | $OPENSSL s_client -connect testssl.sh:443 -servername testssl.sh -no_ssl2 -no_ticket -reconnect 2>&1 | grep -E 'New|Reused|Session-ID'
+#
+# FIXME: actually Ivan's version seems faster. Worth to check and since when -reconnect is a/v
+#
+sub_session_resumption() {
+ local ret ret1 ret2
+ local tmpfile=$(mktemp $TEMPDIR/session_resumption.$NODEIP.XXXXXX)
+ local sess_data=$(mktemp $TEMPDIR/sub_session_data_resumption.$NODEIP.XXXXXX)
+ local -a rw_line
+ local not_new_reused=false
+ local protocol="$1"
+
+ if [[ "$2" == ID ]]; then
+ local byID=true
+ local addcmd="-no_ticket"
+ else
+ local byID=false
+ local addcmd=""
+ if ! "$TLS_TICKETS"; then
+ return 1
+ fi
+ fi
+ "$CLIENT_AUTH" && return 6
+ if "$HAS_NO_SSL2"; then
+ addcmd+=" -no_ssl2"
+ else
+ protocol=${protocol/\./_}
+ protocol=${protocol/v/}
+ protocol="-$(tolower $protocol)"
+ # In some cases a server will not support session tickets, but will support session resumption
+ # by ID. In such a case, it may be more likely to support session resumption with TLSv1.2 than
+ # with TLSv1.3. So, if testing a server that does not support session tickets and that supports
+ # both TLSv1.3 and TLSv1.2 for session resumption by ID, then use a TLSv1.2 ClientHello. (Note that
+ # the line below assumes that if $protocol is -tls1_3, then the server either supports TLSv1.2 or
+ # is TLSv1.3-only.
+ ! "$TLS_TICKETS" && "$byID" && [[ $(has_server_protocol "tls1_2") -eq 0 ]] && protocol="-tls1_2"
+ addcmd+=" $protocol"
+ fi
+
+ $OPENSSL s_client $(s_client_options "$STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY $SNI $addcmd -sess_out $sess_data") </dev/null &>/dev/null
+ ret1=$?
+ if [[ $ret1 -ne 0 ]]; then
+ debugme echo -n "Couldn't connect #1 "
+ return 7
+ fi
+ if "$byID" && [[ ! "$OSSL_NAME" =~ LibreSSL ]] && \
+ ( [[ $OSSL_VER_MAJOR.$OSSL_VER_MINOR == 1.1.1* ]] || [[ $OSSL_VER_MAJOR.$OSSL_VER_MINOR == 3.0.0* ]] ) && \
+ [[ ! -s "$sess_data" ]]; then
+ # it seems OpenSSL indicates no Session ID resumption by just not generating output
+ debugme echo -n "No session resumption byID (empty file)"
+ ret=2
+ else
+ $OPENSSL s_client $(s_client_options "$STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY $SNI $addcmd -sess_in $sess_data") </dev/null >$tmpfile 2>$ERRFILE
+ ret2=$?
+ if [[ $DEBUG -ge 2 ]]; then
+ echo -n "$ret1, $ret2, "
+ [[ -s "$sess_data" ]] && echo "not empty" || echo "empty"
+ fi
+ if [[ $ret2 -ne 0 ]]; then
+ debugme echo -n "Couldn't connect #2 "
+ return 7
+ fi
+ # "Reused" indicates session material was reused, "New": not
+ if grep -aq "^Reused" "$tmpfile"; then
+ new_sid=false
+ elif grep -aq "^New" "$tmpfile"; then
+ new_sid=true
+ else
+ debugme echo -n "Problem with 2nd ServerHello "
+ not_new_reused=true
+ fi
+ # Now get the line and compare the numbers "read" and "written" as a second criteria.
+ # If the "read" number is bigger: a new session ID was probably used
+ rw_line="$(awk '/^SSL handshake has read/ { print $5" "$(NF-1) }' "$tmpfile" )"
+ rw_line=($rw_line)
+ if [[ "${rw_line[0]}" -gt "${rw_line[1]}" ]]; then
+ new_sid2=true
+ else
+ new_sid2=false
+ fi
+ debugme echo "${rw_line[0]}, ${rw_line[1]}"
+
+ if "$new_sid2" && "$new_sid"; then
+ debugme echo -n "No session resumption "
+ ret=1
+ elif ! "$new_sid2" && ! "$new_sid"; then
+ debugme echo -n "Session resumption "
+ ret=0
+ else
+ debugme echo -n "unclear status: $ret1, $ret2, $new_sid, $new_sid2 -- "
+ ret=5
+ fi
+ if [[ $DEBUG -ge 2 ]]; then
+ "$byID" && echo "byID" || echo "by ticket"
+ fi
+ fi
+ "$byID" && \
+ tmpfile_handle ${FUNCNAME[0]}.byID.log $tmpfile || \
+ tmpfile_handle ${FUNCNAME[0]}.byticket.log $tmpfile
+ return $ret
+}
+
+run_server_preference() {
+ local cipher1="" cipher2="" tls13_cipher1="" tls13_cipher2="" default_proto=""
+ local prev_cipher="" default_cipher=""
+ local limitedsense="" supported_sslv2_ciphers
+ local -a cipher proto
+ local proto_ossl proto_txt proto_hex cipherlist i
+ local -i ret=0 j sclient_success str_len
+ local list_fwd="DHE-RSA-SEED-SHA:SEED-SHA:DES-CBC3-SHA:RC4-MD5:DES-CBC-SHA:RC4-SHA:AES128-SHA:AES128-SHA256:AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA:ECDH-RSA-DES-CBC3-SHA:ECDH-RSA-AES128-SHA:ECDH-RSA-AES256-SHA:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:DHE-DSS-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:AES256-SHA256:ECDHE-RSA-DES-CBC3-SHA:ECDHE-RSA-AES128-SHA256:AES256-GCM-SHA384:AES128-GCM-SHA256:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-SHA256:ADH-AES256-GCM-SHA384:AECDH-AES128-SHA:ECDHE-RSA-RC4-SHA:ECDHE-ECDSA-AES128-SHA"
+ local list_reverse="ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-RC4-SHA:AECDH-AES128-SHA:ADH-AES256-GCM-SHA384:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-GCM-SHA256:AES128-GCM-SHA256:AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-DES-CBC3-SHA:AES256-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-DSS-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDH-RSA-AES256-SHA:ECDH-RSA-AES128-SHA:ECDH-RSA-DES-CBC3-SHA:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-AES128-SHA:AES256-SHA:AES128-SHA256:AES128-SHA:RC4-SHA:DES-CBC-SHA:RC4-MD5:DES-CBC3-SHA:SEED-SHA:DHE-RSA-SEED-SHA"
+ tls_list_fwd="c0,2c, c0,30, 00,9f, cc,a9, cc,a8, cc,aa, c0,2b, c0,2f, 00,9e, c0,24, c0,28, 00,6b, c0,23, c0,27, 00,67, c0,0a, 00,04, 00,05, 00,09, 00,0a, 00,9a, 00,96,
+ c0,14, 00,39, c0,09, c0,13, 00,33, 00,9d, 00,9c, 13,01, 13,02, 13,03, 13,04, 13,05, 00,3d, 00,3c, 00,35, 00,2f, 00,ff"
+ tls_list_rev="00,2f, 00,35, 00,3c, 00,3d, 13,05, 13,04, 13,03, 13,02, 13,01, 00,9c, 00,9d, 00,33, c0,13, c0,09, 00,39, c0,14, 00,96, 00,9a, 00,0a, 00,09, 00,05, 00,04,
+ c0,0a, 00,67, c0,27, c0,23, 00,6b, c0,28, c0,24, 00,9e, c0,2f, c0,2b, cc,aa, cc,a8, cc,a9, 00,9f, c0,30, c0,2c, 00,ff"
+ local has_cipher_order=false has_tls13_cipher_order=false
+ local addcmd="" addcmd2=""
+ local using_sockets=true
+ local jsonID="cipher_order"
+ local cwe="CWE-310"
+ local cve=""
+
+ "$SSL_NATIVE" && using_sockets=false
+
+ outln
+ pr_headlineln " Testing server preferences "
+
+ outln
+ pr_bold " Has server cipher order? "
+
+ if [[ "$OPTIMAL_PROTO" == -ssl2 ]]; then
+ addcmd="$OPTIMAL_PROTO"
+ else
+ # the supplied openssl will send an SSLv2 ClientHello if $SNI is empty
+ # and the -no_ssl2 isn't provided.
+ addcmd="-no_ssl2 $SNI"
+ fi
+
+ # Determine negotiated protocol upfront
+ sclient_success=1
+ if "$using_sockets" && [[ $(has_server_protocol "tls1_3") -ne 1 ]]; then
+ # Send similar list of cipher suites as OpenSSL 1.1.1 does
+ tls_sockets "04" \
+ "c0,2c, c0,30, 00,9f, cc,a9, cc,a8, cc,aa, c0,2b, c0,2f, 00,9a, 00,96,
+ 00,9e, c0,24, c0,28, 00,6b, c0,23, c0,27, 00,67, c0,0a,
+ c0,14, 00,39, c0,09, c0,13, 00,33, 00,9d, 00,9c, 13,02,
+ 13,03, 13,01, 13,04, 13,05, 00,3d, 00,3c, 00,35, 00,2f, 00,ff" \
+ "ephemeralkey"
+ sclient_success=$?
+ if [[ $sclient_success -eq 0 ]]; then
+ add_tls_offered tls1_3 yes
+ elif [[ $sclient_success -eq 2 ]]; then
+ sclient_success=0 # 2: downgraded
+ case $DETECTED_TLS_VERSION in
+ 0303) add_tls_offered tls1_2 yes ;;
+ 0302) add_tls_offered tls1_1 yes ;;
+ 0301) add_tls_offered tls1 yes ;;
+ 0300) add_tls_offered ssl3 yes ;;
+ esac
+ fi
+ if [[ $sclient_success -eq 0 ]] ; then
+ cp "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt" $TMPFILE
+ cp "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt" "$TEMPDIR/$NODEIP.parse_tls13_serverhello.txt"
+ cipher0=$(get_cipher $TMPFILE)
+ fi
+ fi
+ if [[ $sclient_success -ne 0 ]]; then
+ $OPENSSL s_client $(s_client_options "$STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY $addcmd") </dev/null 2>>$ERRFILE >$TMPFILE
+ if sclient_connect_successful $? $TMPFILE; then
+ cipher0=$(get_cipher $TMPFILE)
+ debugme tm_out "0 --> $cipher0\n"
+ cp $TMPFILE "$TEMPDIR/$NODEIP.parse_tls13_serverhello.txt"
+ else
+ # 2 second try with $OPTIMAL_PROTO especially for intolerant IIS6 servers:
+ $OPENSSL s_client $(s_client_options "$STARTTLS $OPTIMAL_PROTO $BUGS -connect $NODEIP:$PORT $PROXY $SNI") </dev/null 2>>$ERRFILE >$TMPFILE
+ if ! sclient_connect_successful $? $TMPFILE; then
+ pr_warning "Handshake error!"
+ ret=1
+ fi
+ fi
+ fi
+ default_proto=$(get_protocol $TMPFILE)
+ [[ "$default_proto" == TLSv1.0 ]] && default_proto="TLSv1"
+ # debugme tm_out " --> $default_proto\n"
+
+ # Some servers don't have a TLS 1.3 cipher order, see #1163
+ if [[ "$default_proto" == TLSv1.3 ]]; then
+ tls_sockets "04" "13,05, 13,04, 13,03, 13,02, 13,01, 00,ff"
+ [[ $? -ne 0 ]] && ret=1 && prln_fixme "something weird happened around line $((LINENO - 1))"
+ cp "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt" $TMPFILE
+ tls13_cipher1=$(get_cipher $TMPFILE)
+ debugme tm_out "TLS 1.3: --> $tls13_cipher1\n"
+ tls_sockets "04" "13,01, 13,02, 13,03, 13,04, 13,05, 00,ff"
+ [[ $? -ne 0 ]] && ret=1 && prln_fixme "something weird happened around line $((LINENO - 1))"
+ cp "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt" $TMPFILE
+ tls13_cipher2=$(get_cipher $TMPFILE)
+ debugme tm_out "TLS 1.3: --> $tls13_cipher2\n"
+
+ [[ $tls13_cipher1 == $tls13_cipher2 ]] && has_tls13_cipher_order=true
+ fi
+ # Check whether the server has a cipher order for SSLv3 - TLSv1.2
+ if [[ $(has_server_protocol "tls1_2") -ne 0 ]] && [[ $(has_server_protocol "tls1_1") -ne 0 ]] && \
+ [[ $(has_server_protocol "tls1") -ne 0 ]] && [[ $(has_server_protocol "ssl3") -ne 0 ]]; then
+ # Based on testing performed by determine_optimal_sockets_params(), it is believed that
+ # this server does not offer SSLv3 - TLSv1.2.
+ has_cipher_order="$has_tls13_cipher_order"
+ elif [[ "$OPTIMAL_PROTO" != -ssl2 ]]; then
+ if [[ -n "$STARTTLS_OPTIMAL_PROTO" ]]; then
+ [[ ! "$STARTTLS_OPTIMAL_PROTO" =~ ssl ]] && addcmd2="$SNI"
+ [[ "$STARTTLS_OPTIMAL_PROTO" != -tls1_3 ]] && addcmd2+=" $STARTTLS_OPTIMAL_PROTO"
+ else
+ addcmd2="-no_ssl2 $SNI"
+ fi
+ [[ $DEBUG -ge 4 ]] && echo -e "\n Forward: ${list_fwd}"
+ $OPENSSL s_client $(s_client_options "$STARTTLS -cipher $list_fwd $BUGS -connect $NODEIP:$PORT $PROXY $addcmd2") </dev/null 2>$ERRFILE >$TMPFILE
+ if ! sclient_connect_successful $? $TMPFILE; then
+ list_fwd="$(actually_supported_osslciphers $list_fwd '' '-tls1')"
+ pr_warning "no matching cipher in this list found (pls report this): "
+ outln "$list_fwd . "
+ fileout "$jsonID" "WARN" "Could not determine server cipher order, no matching cipher in list found (pls report this): $list_fwd"
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ # we assume the problem is with testing here but it could be also the server side
+ fi
+ cipher1=$(get_cipher $TMPFILE) # cipher1 from 1st serverhello
+ debugme tm_out "1 --> $cipher1\n"
+
+ # second client hello with reverse list
+ [[ $DEBUG -ge 4 ]] && echo -e "\n Reverse: ${list_reverse}"
+ $OPENSSL s_client $(s_client_options "$STARTTLS -cipher $list_reverse $BUGS -connect $NODEIP:$PORT $PROXY $addcmd2") </dev/null 2>>$ERRFILE >$TMPFILE
+ # first handshake worked above so no error handling here
+ cipher2=$(get_cipher $TMPFILE) # cipher2 from 2nd serverhello
+ debugme tm_out "2 --> $cipher2\n"
+
+ [[ $cipher1 == $cipher2 ]] && has_cipher_order=true
+ fi
+ debugme echo "has_cipher_order: $has_cipher_order"
+ debugme echo "has_tls13_cipher_order: $has_tls13_cipher_order"
+
+ if "$TLS13_ONLY" && ! "$has_tls13_cipher_order"; then
+ out "no (TLS 1.3 only)"
+ limitedsense=" (limited sense as client will pick)"
+ fileout "$jsonID" "INFO" "not a cipher order for TLS 1.3 configured"
+ elif ! "$has_cipher_order" && ! "$has_tls13_cipher_order"; then
+ # server used the different ends (ciphers) from the client hello
+ pr_svrty_high "no (NOT ok)"
+ limitedsense=" (limited sense as client will pick)"
+ fileout "$jsonID" "HIGH" "NOT a cipher order configured"
+ elif "$has_cipher_order" && ! "$has_tls13_cipher_order" && [[ "$default_proto" == TLSv1.3 ]]; then
+ pr_svrty_good "yes (OK)"; out " -- only for < TLS 1.3"
+ fileout "$jsonID" "OK" "server -- TLS 1.3 client determined"
+ elif ! "$has_cipher_order" && "$has_tls13_cipher_order"; then
+ pr_svrty_high "no (NOT ok)"; out " -- only for TLS 1.3"
+ fileout "$jsonID" "HIGH" "server -- < TLS 1.3 client determined"
+ else
+ if "$has_tls13_cipher_order"; then
+ if "$TLS13_ONLY"; then
+ out "yes (TLS 1.3 only)"
+ fileout "$jsonID" "INFO" "server (TLS 1.3)"
+ else
+ pr_svrty_best "yes (OK)"
+ out " -- TLS 1.3 and below"
+ fileout "$jsonID" "OK" "server"
+ fi
+ else
+ # we don't have TLS 1.3 at all
+ pr_svrty_best "yes (OK)"
+ fileout "$jsonID" "OK" "server"
+ fi
+ fi
+ outln
+
+ pr_bold " Negotiated protocol "
+ jsonID="protocol_negotiated"
+
+ case "$default_proto" in
+ *TLSv1.3)
+ prln_svrty_best $default_proto
+ fileout "$jsonID" "OK" "Default protocol TLS1.3"
+ ;;
+ *TLSv1.2)
+ prln_svrty_best $default_proto
+ fileout "$jsonID" "OK" "Default protocol TLS1.2"
+ ;;
+ *TLSv1.1)
+ prln_svrty_low $default_proto
+ fileout "$jsonID" "LOW" "Default protocol TLS1.1"
+ ;;
+ *TLSv1)
+ prln_svrty_low $default_proto
+ fileout "$jsonID" "LOW" "Default protocol TLS1.0"
+ ;;
+ *SSLv2)
+ prln_svrty_critical $default_proto
+ fileout "$jsonID" "CRITICAL" "Default protocol SSLv2"
+ ;;
+ *SSLv3)
+ prln_svrty_critical $default_proto
+ fileout "$jsonID" "CRITICAL" "Default protocol SSLv3"
+ ;;
+ "")
+ pr_warning "default proto empty"
+ if [[ $OSSL_VER == 1.0.2* ]]; then
+ outln " (Hint: if IIS6 give OpenSSL 1.0.1 a try)"
+ fileout "$jsonID" "WARN" "Default protocol empty (Hint: if IIS6 give OpenSSL 1.0.1 a try)"
+ else
+ outln
+ fileout "$jsonID" "WARN" "Default protocol empty"
+ fi
+ ret=1
+ ;;
+ *)
+ pr_warning "FIXME line $LINENO: $default_proto"
+ fileout "$jsonID" "WARN" "FIXME line $LINENO: $default_proto"
+ ret=1
+ ;;
+ esac
+
+ pr_bold " Negotiated cipher "
+ jsonID="cipher_negotiated"
+
+ # restore file from above
+ [[ "$default_proto" == TLSv1.3 ]] && cp "$TEMPDIR/$NODEIP.parse_tls13_serverhello.txt" $TMPFILE
+ cipher1=$(get_cipher $TMPFILE)
+
+ # Sanity check: Handshake with no ciphers and one with forward list didn't overlap
+ if [[ "$cipher0" != $cipher1 ]]; then
+ limitedsense=" (matching cipher in list missing)"
+ fi
+
+ if [[ "$DISPLAY_CIPHERNAMES" =~ openssl ]] && ( [[ "$cipher1" == TLS_* ]] || [[ "$cipher1" == SSL_* ]] ); then
+ default_cipher="$(rfc2openssl "$cipher1")"
+ elif [[ "$DISPLAY_CIPHERNAMES" =~ rfc ]] && [[ "$cipher1" != TLS_* ]] && [[ "$cipher1" != SSL_* ]]; then
+ default_cipher="$(openssl2rfc "$cipher1")"
+ fi
+ [[ -z "$default_cipher" ]] && default_cipher="$cipher1"
+ pr_cipher_quality "$default_cipher"
+ case $? in
+ 1) fileout "$jsonID" "CRITICAL" "$default_cipher$(read_dhbits_from_file "$TMPFILE" "string") $limitedsense"
+ ;;
+ 2) fileout "$jsonID" "HIGH" "$default_cipher$(read_dhbits_from_file "$TMPFILE" "string") $limitedsense"
+ ;;
+ 3) fileout "$jsonID" "MEDIUM" "$default_cipher$(read_dhbits_from_file "$TMPFILE" "string") $limitedsense"
+ ;;
+ 6|7) fileout "$jsonID" "OK" "$default_cipher$(read_dhbits_from_file "$TMPFILE" "string") $limitedsense"
+ ;; # best ones
+ 4) fileout "$jsonID" "LOW" "$default_cipher$(read_dhbits_from_file "$TMPFILE" "string") (cbc) $limitedsense"
+ ;; # it's CBC. --> lucky13
+ 0) pr_warning "default cipher empty" ;
+ if [[ $OSSL_VER == 1.0.2* ]]; then
+ out " (Hint: if IIS6 give OpenSSL 1.0.1 a try)"
+ fileout "$jsonID" "WARN" "Default cipher empty (if IIS6 give OpenSSL 1.0.1 a try) $limitedsense"
+ else
+ fileout "$jsonID" "WARN" "Default cipher empty $limitedsense"
+ fi
+ ret=1
+ ;;
+ *) fileout "$jsonID" "INFO" "$default_cipher$(read_dhbits_from_file "$TMPFILE" "string") $limitedsense"
+ ;;
+ esac
+ read_dhbits_from_file "$TMPFILE"
+
+ if [[ "$cipher0" != $cipher1 ]]; then
+ pr_warning " -- inconclusive test, matching cipher in list missing"
+ outln ", better see below"
+ #FIXME: This is ugly but the best we can do before rewrite this section
+ else
+ outln "$limitedsense"
+ fi
+
+ if "$has_cipher_order"; then
+ "$FAST" && using_sockets=false
+ [[ $TLS_NR_CIPHERS == 0 ]] && using_sockets=false
+
+ pr_bold " Cipher order"
+ while read proto_ossl proto_hex proto_txt; do
+ [[ "$proto_ossl" == tls1_3 ]] && ! "$has_tls13_cipher_order" && continue
+ cipher_pref_check "$proto_ossl" "$proto_hex" "$proto_txt" "$using_sockets"
+ done <<< "$(tm_out " ssl3 00 SSLv3\n tls1 01 TLSv1\n tls1_1 02 TLSv1.1\n tls1_2 03 TLSv1.2\n tls1_3 04 TLSv1.3\n")"
+ outln
+ outln
+ else
+ pr_bold " Negotiated cipher per proto"; outln " $limitedsense"
+ i=1
+ for proto_ossl in ssl2 ssl3 tls1 tls1_1 tls1_2 tls1_3; do
+ if [[ $proto_ossl == ssl2 ]] && ! "$HAS_SSL2"; then
+ if ! "$using_sockets" || [[ $TLS_NR_CIPHERS -eq 0 ]]; then
+ out " (SSLv2: "; pr_local_problem "$OPENSSL doesn't support \"s_client -ssl2\""; outln ")";
+ continue
+ else
+ sslv2_sockets "" "true"
+ if [[ $? -eq 3 ]] && [[ "$V2_HELLO_CIPHERSPEC_LENGTH" -ne 0 ]]; then
+ # Just arbitrarily pick the first cipher in the cipher-mapping.txt list.
+ proto[i]="SSLv2"
+ supported_sslv2_ciphers="$(grep "Supported cipher: " "$TEMPDIR/$NODEIP.parse_sslv2_serverhello.txt")"
+ for (( j=0; j < TLS_NR_CIPHERS; j++ )); do
+ if [[ "${TLS_CIPHER_SSLVERS[j]}" == "SSLv2" ]]; then
+ cipher1="${TLS_CIPHER_HEXCODE[j]}"
+ cipher1="$(tolower "x${cipher1:2:2}${cipher1:7:2}${cipher1:12:2}")"
+ if [[ "$supported_sslv2_ciphers" =~ $cipher1 ]]; then
+ if ( [[ "$DISPLAY_CIPHERNAMES" =~ openssl ]] && [[ "${TLS_CIPHER_OSSL_NAME[j]}" != "-" ]] ) || [[ "${TLS_CIPHER_RFC_NAME[j]}" == "-" ]]; then
+ cipher[i]="${TLS_CIPHER_OSSL_NAME[j]}"
+ else
+ cipher[i]="${TLS_CIPHER_RFC_NAME[j]}"
+ fi
+ break
+ fi
+ fi
+ done
+ [[ $DEBUG -ge 2 ]] && tmln_out "Default cipher for ${proto[i]}: ${cipher[i]}"
+ else
+ proto[i]=""
+ cipher[i]=""
+ fi
+ fi
+ elif ( [[ $proto_ossl == ssl3 ]] && ! "$HAS_SSL3" ) || ( [[ $proto_ossl == tls1_3 ]] && ! "$HAS_TLS13" ); then
+ if [[ $proto_ossl == ssl3 ]]; then
+ proto_txt="SSLv3" ; proto_hex="00" ; cipherlist="$TLS_CIPHER"
+ else
+ proto_txt="TLSv1.3" ; proto_hex="04" ; cipherlist="$TLS13_CIPHER"
+ fi
+ if ! "$using_sockets"; then
+ out " ($proto_txt: "; pr_local_problem "$OPENSSL doesn't support \"s_client -$proto_ossl\"" ; outln ")";
+ continue
+ else
+ tls_sockets "$proto_hex" "$cipherlist"
+ if [[ $? -eq 0 ]]; then
+ proto[i]="$proto_txt"
+ cipher1=$(get_cipher "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt")
+ cipher[i]="$cipher1"
+ if [[ "$DISPLAY_CIPHERNAMES" =~ openssl ]] && [[ $TLS_NR_CIPHERS -ne 0 ]]; then
+ cipher[i]="$(rfc2openssl "$cipher1")"
+ [[ -z "${cipher[i]}" ]] && cipher[i]="$cipher1"
+ fi
+ [[ $DEBUG -ge 2 ]] && tmln_out "Default cipher for ${proto[i]}: ${cipher[i]}"
+ else
+ proto[i]=""
+ cipher[i]=""
+ fi
+ fi
+ else
+ $OPENSSL s_client $(s_client_options "$STARTTLS -"$proto_ossl" $BUGS -connect $NODEIP:$PORT $PROXY $SNI") </dev/null 2>>$ERRFILE >$TMPFILE
+ if sclient_connect_successful $? $TMPFILE; then
+ proto[i]=$(get_protocol $TMPFILE)
+ cipher[i]=$(get_cipher $TMPFILE)
+ [[ ${cipher[i]} == "0000" ]] && cipher[i]="" # Hack!
+ if [[ "$DISPLAY_CIPHERNAMES" =~ rfc ]] && [[ -n "${cipher[i]}" ]]; then
+ cipher[i]="$(openssl2rfc "${cipher[i]}")"
+ [[ -z "${cipher[i]}" ]] && cipher[i]=$(get_cipher $TMPFILE)
+ fi
+ [[ $DEBUG -ge 2 ]] && tmln_out "Default cipher for ${proto[i]}: ${cipher[i]}"
+ else
+ proto[i]=""
+ cipher[i]=""
+ fi
+ fi
+ [[ -n "${cipher[i]}" ]] && add_tls_offered "$proto_ossl" yes
+ i=$((i + 1))
+ done
+
+ for i in 1 2 3 4 5 6; do
+ if [[ -n "${cipher[i]}" ]]; then # cipher not empty
+ if [[ -z "$prev_cipher" ]] || [[ "$prev_cipher" != "${cipher[i]}" ]]; then
+ [[ -n "$prev_cipher" ]] && outln
+ str_len=${#cipher[i]}
+ out " "
+ if [[ "$COLOR" -le 2 ]]; then
+ out "${cipher[i]}"
+ else
+ pr_cipher_quality "${cipher[i]}"
+ fi
+ out ":"
+ if [[ "$DISPLAY_CIPHERNAMES" =~ openssl ]]; then
+ for (( 1; str_len < 30; str_len++ )); do
+ out " "
+ done
+ else
+ for (( 1; str_len < 51; str_len++ )); do
+ out " "
+ done
+ fi
+ else
+ out ", " # same cipher --> only print out protocol behind it
+ fi
+ out "${proto[i]}"
+ prev_cipher="${cipher[i]}"
+ fi
+ fileout "cipher_order_${proto[i]}" "INFO" "${cipher[i]} at ${proto[i]} $limitedsense"
+ done
+ outln "\n No further cipher order check has been done as order is determined by the client"
+ outln
+ fi
+ return $ret
+}
+
+check_tls12_pref() {
+ local batchremoved="-CAMELLIA:-IDEA:-KRB5:-PSK:-SRP:-aNULL:-eNULL"
+ local batchremoved_success=false
+ local tested_cipher="" cipher ciphers_to_test
+ local order=""
+ local -i nr_ciphers_found_r1=0 nr_ciphers_found_r2=0
+
+ while true; do
+ $OPENSSL s_client $(s_client_options "$STARTTLS -tls1_2 $BUGS -cipher "ALL$tested_cipher:$batchremoved" -connect $NODEIP:$PORT $PROXY $SNI") </dev/null 2>>$ERRFILE >$TMPFILE
+ if sclient_connect_successful $? $TMPFILE ; then
+ cipher=$(get_cipher $TMPFILE)
+ order+=" $cipher"
+ tested_cipher="$tested_cipher:-$cipher"
+ nr_ciphers_found_r1+=1
+ "$FAST" && break
+ else
+ debugme tmln_out "A: $tested_cipher"
+ break
+ fi
+ done
+ batchremoved="${batchremoved//-/}"
+ while true; do
+ # no ciphers from "ALL$tested_cipher:$batchremoved" left
+ # now we check $batchremoved, and remove the minus signs first:
+ $OPENSSL s_client $(s_client_options "$STARTTLS -tls1_2 $BUGS -cipher "$batchremoved" -connect $NODEIP:$PORT $PROXY $SNI") </dev/null 2>>$ERRFILE >$TMPFILE
+ if sclient_connect_successful $? $TMPFILE ; then
+ batchremoved_success=true # signals that we have some of those ciphers and need to put everything together later on
+ cipher=$(get_cipher $TMPFILE)
+ order+=" $cipher"
+ batchremoved="$batchremoved:-$cipher"
+ nr_ciphers_found_r1+=1
+ debugme tmln_out "B1: $batchremoved"
+ "$FAST" && break
+ else
+ debugme tmln_out "B2: $batchremoved"
+ break
+ # nothing left with batchremoved ciphers, we need to put everything together
+ fi
+ done
+
+ if "$batchremoved_success"; then
+ # now we combine the two cipher sets from both while loops
+ combined_ciphers="$order"
+ order="" ; tested_cipher=""
+ while true; do
+ ciphers_to_test=""
+ for cipher in $combined_ciphers; do
+ [[ ! "$tested_cipher:" =~ :-$cipher: ]] && ciphers_to_test+=":$cipher"
+ done
+ [[ -z "$ciphers_to_test" ]] && break
+ $OPENSSL s_client $(s_client_options "$STARTTLS -tls1_2 $BUGS -cipher "${ciphers_to_test:1}" -connect $NODEIP:$PORT $PROXY $SNI") </dev/null 2>>$ERRFILE >$TMPFILE
+ if sclient_connect_successful $? $TMPFILE ; then
+ cipher=$(get_cipher $TMPFILE)
+ order+=" $cipher"
+ tested_cipher="$tested_cipher:-$cipher"
+ nr_ciphers_found_r2+=1
+ "$FAST" && break
+ else
+ # This shouldn't happen.
+ break
+ fi
+ done
+ if "$FAST" && [[ $nr_ciphers_found_r2 -ne 1 ]]; then
+ prln_fixme "something weird happened around line $((LINENO - 14))"
+ return 1
+ elif ! "$FAST" && [[ $nr_ciphers_found_r2 -ne $nr_ciphers_found_r1 ]]; then
+ prln_fixme "something weird happened around line $((LINENO - 16))"
+ return 1
+ fi
+ fi
+ tm_out "$order"
+
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 0
+}
+
+
+cipher_pref_check() {
+ local p="$1" proto_hex="$2" proto="$3"
+ local using_sockets="$4"
+ local tested_cipher cipher order rfc_cipher rfc_order
+ local overflow_probe_cipherlist="ALL:-ECDHE-RSA-AES256-GCM-SHA384:-AES128-SHA:-DES-CBC3-SHA"
+ local -i i nr_ciphers nr_nonossl_ciphers num_bundles mod_check bundle_size bundle end_of_bundle success
+ local hexc ciphers_to_test
+ local -a rfc_ciph hexcode ciphers_found ciphers_found2
+ local -a -i index
+ local ciphers_found_with_sockets
+
+ order=""; ciphers_found_with_sockets=false
+ if [[ $p == ssl3 ]] && ! "$HAS_SSL3" && ! "$using_sockets"; then
+ out "\n SSLv3: "; pr_local_problem "$OPENSSL doesn't support \"s_client -ssl3\"";
+ return 0
+ fi
+ if [[ $p == tls1_3 ]] && ! "$HAS_TLS13" && ! "$using_sockets"; then
+ out "\n TLSv1.3 "; pr_local_problem "$OPENSSL doesn't support \"s_client -tls1_3\"";
+ return 0
+ fi
+
+ [[ $(has_server_protocol "$p") -eq 1 ]] && return 0
+
+ if ( [[ $p != tls1_3 ]] || "$HAS_TLS13" ) && ( [[ $p != ssl3 ]] || "$HAS_SSL3" ); then
+ if [[ $p == tls1_2 ]] && "$SERVER_SIZE_LIMIT_BUG"; then
+ order="$(check_tls12_pref)"
+ else
+ tested_cipher=""
+ while true; do
+ if [[ $p != tls1_3 ]]; then
+ ciphers_to_test="-cipher ALL:COMPLEMENTOFALL${tested_cipher}"
+ else
+ ciphers_to_test=""
+ for cipher in $(colon_to_spaces "$TLS13_OSSL_CIPHERS"); do
+ [[ ! "$tested_cipher" =~ ":-"$cipher ]] && ciphers_to_test+=":$cipher"
+ done
+ [[ -z "$ciphers_to_test" ]] && break
+ ciphers_to_test="-ciphersuites ${ciphers_to_test:1}"
+ fi
+ $OPENSSL s_client $(s_client_options "$STARTTLS -"$p" $BUGS $ciphers_to_test -connect $NODEIP:$PORT $PROXY $SNI") </dev/null 2>>$ERRFILE >$TMPFILE
+ sclient_connect_successful $? $TMPFILE || break
+ cipher=$(get_cipher $TMPFILE)
+ [[ -z "$cipher" ]] && break
+ order+="$cipher "
+ tested_cipher+=":-"$cipher
+ "$FAST" && break
+ done
+ fi
+ fi
+
+ nr_nonossl_ciphers=0
+ if "$using_sockets"; then
+ for (( i=0; i < TLS_NR_CIPHERS; i++ )); do
+ ciphers_found[i]=false
+ hexc="${TLS_CIPHER_HEXCODE[i]}"
+ if [[ ${#hexc} -eq 9 ]]; then
+ if [[ " $order " =~ " ${TLS_CIPHER_OSSL_NAME[i]} " ]]; then
+ ciphers_found[i]=true
+ else
+ ciphers_found2[nr_nonossl_ciphers]=false
+ hexcode[nr_nonossl_ciphers]="${hexc:2:2},${hexc:7:2}"
+ rfc_ciph[nr_nonossl_ciphers]="${TLS_CIPHER_RFC_NAME[i]}"
+ index[nr_nonossl_ciphers]=$i
+ # Only test ciphers that are relevant to the protocol.
+ if [[ "$p" == tls1_3 ]]; then
+ [[ "${hexc:2:2}" == "13" ]] && nr_nonossl_ciphers+=1
+ elif [[ "$p" == tls1_2 ]]; then
+ [[ "${hexc:2:2}" != 13 ]] && nr_nonossl_ciphers+=1
+ elif [[ ! "${TLS_CIPHER_RFC_NAME[i]}" =~ SHA256 ]] && \
+ [[ ! "${TLS_CIPHER_RFC_NAME[i]}" =~ SHA384 ]] && \
+ [[ "${TLS_CIPHER_RFC_NAME[i]}" != *"_CCM" ]] && \
+ [[ "${TLS_CIPHER_RFC_NAME[i]}" != *"_CCM_8" ]]; then
+ nr_nonossl_ciphers+=1
+ fi
+ fi
+ fi
+ done
+ fi
+
+ if [[ $nr_nonossl_ciphers -eq 0 ]]; then
+ num_bundles=0
+ elif [[ $p != tls1_2 ]] || ! "$SERVER_SIZE_LIMIT_BUG"; then
+ num_bundles=1
+ bundle_size=$nr_nonossl_ciphers
+ else
+ num_bundles=$nr_nonossl_ciphers/128
+ mod_check=$nr_nonossl_ciphers%128
+ [[ $mod_check -ne 0 ]] && num_bundles=$num_bundles+1
+
+ bundle_size=$nr_nonossl_ciphers/$num_bundles
+ mod_check=$nr_nonossl_ciphers%$num_bundles
+ [[ $mod_check -ne 0 ]] && bundle_size+=1
+ fi
+
+ for (( bundle=0; bundle < num_bundles; bundle++ )); do
+ end_of_bundle=$bundle*$bundle_size+$bundle_size
+ [[ $end_of_bundle -gt $nr_nonossl_ciphers ]] && end_of_bundle=$nr_nonossl_ciphers
+ while true; do
+ ciphers_to_test=""
+ for (( i=bundle*bundle_size; i < end_of_bundle; i++ )); do
+ ! "${ciphers_found2[i]}" && ciphers_to_test+=", ${hexcode[i]}"
+ done
+ [[ -z "$ciphers_to_test" ]] && break
+ tls_sockets "$proto_hex" "${ciphers_to_test:2}, 00,ff" "ephemeralkey"
+ [[ $? -ne 0 ]] && break
+ cipher=$(get_cipher "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt")
+ for (( i=bundle*bundle_size; i < end_of_bundle; i++ )); do
+ [[ "$cipher" == "${rfc_ciph[i]}" ]] && ciphers_found2[i]=true && break
+ done
+ i=${index[i]}
+ ciphers_found[i]=true
+ ciphers_found_with_sockets=true
+ if [[ $p != tls1_2 ]] || ! "$SERVER_SIZE_LIMIT_BUG"; then
+ # Throw out the results found so far and start over using just sockets
+ bundle=$num_bundles
+ for (( i=0; i < TLS_NR_CIPHERS; i++ )); do
+ ciphers_found[i]=true
+ done
+ break
+ fi
+ done
+ done
+
+ # If additional ciphers were found using sockets and there is no
+ # SERVER_SIZE_LIMIT_BUG, then just use sockets to find the cipher order.
+ # If there is a SERVER_SIZE_LIMIT_BUG, then use sockets to find the cipher
+ # order, but starting with the list of ciphers supported by the server.
+ if "$ciphers_found_with_sockets"; then
+ order=""
+ nr_ciphers=0
+ for (( i=0; i < TLS_NR_CIPHERS; i++ )); do
+ hexc="${TLS_CIPHER_HEXCODE[i]}"
+ if "${ciphers_found[i]}" && [[ ${#hexc} -eq 9 ]]; then
+ ciphers_found2[nr_ciphers]=false
+ hexcode[nr_ciphers]="${hexc:2:2},${hexc:7:2}"
+ rfc_ciph[nr_ciphers]="${TLS_CIPHER_RFC_NAME[i]}"
+ if [[ "$p" == "tls1_3" ]]; then
+ [[ "${hexc:2:2}" == "13" ]] && nr_ciphers+=1
+ elif [[ "$p" == "tls1_2" ]]; then
+ [[ "${hexc:2:2}" != "13" ]] && nr_ciphers+=1
+ elif [[ ! "${TLS_CIPHER_RFC_NAME[i]}" =~ SHA256 ]] && \
+ [[ ! "${TLS_CIPHER_RFC_NAME[i]}" =~ SHA384 ]] && \
+ [[ "${TLS_CIPHER_RFC_NAME[i]}" != *"_CCM" ]] && \
+ [[ "${TLS_CIPHER_RFC_NAME[i]}" != *"_CCM_8" ]]; then
+ nr_ciphers+=1
+ fi
+ fi
+ done
+ while true; do
+ ciphers_to_test=""
+ for (( i=0; i < nr_ciphers; i++ )); do
+ ! "${ciphers_found2[i]}" && ciphers_to_test+=", ${hexcode[i]}"
+ done
+ [[ -z "$ciphers_to_test" ]] && break
+ tls_sockets "$proto_hex" "${ciphers_to_test:2}, 00,ff" "ephemeralkey"
+ [[ $? -ne 0 ]] && break
+ cipher=$(get_cipher "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt")
+ for (( i=0; i < nr_ciphers; i++ )); do
+ [[ "$cipher" == ${rfc_ciph[i]} ]] && ciphers_found2[i]=true && break
+ done
+ if [[ "$DISPLAY_CIPHERNAMES" =~ openssl ]] && [[ $TLS_NR_CIPHERS -ne 0 ]]; then
+ cipher="$(rfc2openssl "$cipher")"
+ # If there is no OpenSSL name for the cipher, then use the RFC name
+ [[ -z "$cipher" ]] && cipher=$(get_cipher "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt")
+ fi
+ order+="$cipher "
+ done
+ elif [[ -n "$order" ]] && [[ "$DISPLAY_CIPHERNAMES" =~ rfc ]]; then
+ rfc_order=""
+ while read -d " " cipher; do
+ rfc_cipher="$(openssl2rfc "$cipher")"
+ if [[ -n "$rfc_cipher" ]]; then
+ rfc_order+="$rfc_cipher "
+ else
+ rfc_order+="$cipher "
+ fi
+ done <<< "$order"
+ order="$rfc_order"
+ fi
+
+ if [[ -n "$order" ]]; then
+ add_tls_offered "$p" yes
+ outln
+ out "$(printf " %-10s " "$proto: ")"
+ if [[ "$COLOR" -le 2 ]]; then
+ out "$(out_row_aligned_max_width "$order" " " $TERM_WIDTH)"
+ else
+ out_row_aligned_max_width_by_entry "$order" " " $TERM_WIDTH pr_cipher_quality
+ fi
+ fileout "cipherorder_${proto//./_}" "INFO" "$order"
+ fi
+
+ tmpfile_handle ${FUNCNAME[0]}-$p.txt
+ return 0
+}
+
+
+# arg1 is OpenSSL s_client parameter or empty
+#
+get_host_cert() {
+ local tmpvar=$TEMPDIR/${FUNCNAME[0]}.txt # change later to $TMPFILE
+
+ $OPENSSL s_client $(s_client_options "$STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY $SNI $1") 2>/dev/null </dev/null >$tmpvar
+ if sclient_connect_successful $? $tmpvar; then
+ awk '/-----BEGIN/,/-----END/ { print $0 }' $tmpvar >$HOSTCERT
+ return 0
+ else
+ if [[ -z "$1" ]]; then
+ prln_warning "could not retrieve host certificate!"
+ fileout "host_certificate_Problem" "WARN" "Could not retrieve host certificate!"
+ fi
+ return 1
+ fi
+ #tmpfile_handle ${FUNCNAME[0]}.txt
+ #return $((${PIPESTATUS[0]} + ${PIPESTATUS[1]}))
+}
+
+verify_retcode_helper() {
+ local ret=0
+ local -i retcode=$1
+
+ case $retcode in
+ # codes from ./doc/apps/verify.pod | verify(1ssl)
+ 44) tm_out "(different CRL scope)" ;; # X509_V_ERR_DIFFERENT_CRL_SCOPE
+ 26) tm_out "(unsupported certificate purpose)" ;; # X509_V_ERR_INVALID_PURPOSE
+ 24) tm_out "(certificate unreadable)" ;; # X509_V_ERR_INVALID_CA
+ 23) tm_out "(certificate revoked)" ;; # X509_V_ERR_CERT_REVOKED
+ 21) tm_out "(chain incomplete, only 1 cert provided)" ;; # X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE
+ 20) tm_out "(chain incomplete)" ;; # X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY
+ 19) tm_out "(self signed CA in chain)" ;; # X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN
+ 18) tm_out "(self signed)" ;; # X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT
+ 10) tm_out "(expired)" ;; # X509_V_ERR_CERT_HAS_EXPIRED
+ 9) tm_out "(not yet valid)" ;; # X509_V_ERR_CERT_NOT_YET_VALID
+ 2) tm_out "(issuer cert missing)" ;; # X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT
+ *) ret=1 ; tm_out " (unknown, pls report) $1" ;;
+ esac
+ return $ret
+}
+
+# arg1: number of certificate if provided >1
+determine_trust() {
+ local jsonID="$1"
+ local json_postfix="$2"
+ local -i i=1
+ local -i num_ca_bundles=0
+ local bundle_fname=""
+ local -a certificate_file verify_retcode trust
+ local ok_was=""
+ local notok_was=""
+ local all_ok=true
+ local some_ok=false
+ local code
+ local ca_bundles=""
+ local spaces=" "
+ local -i certificates_provided=1+$(grep -c "\-\-\-\-\-BEGIN CERTIFICATE\-\-\-\-\-" $TEMPDIR/intermediatecerts.pem)
+ local addtl_warning
+
+ # If $json_postfix is not empty, then there is more than one certificate
+ # and the output should should be indented by two more spaces.
+ [[ -n $json_postfix ]] && spaces=" "
+
+ case $OSSL_VER_MAJOR.$OSSL_VER_MINOR in
+ 1.0.2|1.1.0|1.1.1|2.[1-9].*|3.*) # 2.x is LibreSSL. 2.1.1 was tested to work, below is not sure
+ :
+ ;;
+ *) addtl_warning="Your $OPENSSL <= 1.0.2 might be too unreliable to determine trust"
+ fileout "${jsonID}${json_postfix}" "WARN" "$addtl_warning"
+ addtl_warning="(${addtl_warning})"
+ ;;
+ esac
+ debugme tmln_out
+
+ # if you run testssl.sh from a different path /you can set either TESTSSL_INSTALL_DIR or CA_BUNDLES_PATH to find the CA BUNDLES
+ if [[ -z "$CA_BUNDLES_PATH" ]]; then
+ ca_bundles="$TESTSSL_INSTALL_DIR/etc/*.pem"
+ else
+ ca_bundles="$CA_BUNDLES_PATH/*.pem"
+ fi
+ for bundle_fname in $ca_bundles; do
+ certificate_file[i]=$(basename ${bundle_fname//.pem})
+ if [[ ! -r $bundle_fname ]]; then
+ prln_warning "\"$bundle_fname\" cannot be found / not readable"
+ return 1
+ fi
+ debugme printf -- " %-12s" "${certificate_file[i]}"
+ # Set SSL_CERT_DIR to /dev/null so that $OPENSSL verify will only use certificates in $bundle_fname
+ # in a subshell because that should be valid here only
+ (export SSL_CERT_DIR="/dev/null"; export SSL_CERT_FILE="/dev/null"
+ if [[ $certificates_provided -ge 2 ]]; then
+ $OPENSSL verify -purpose sslserver -CAfile <(cat $ADDITIONAL_CA_FILES "$bundle_fname") -untrusted $TEMPDIR/intermediatecerts.pem $HOSTCERT >$TEMPDIR/${certificate_file[i]}.1 2>$TEMPDIR/${certificate_file[i]}.2
+ else
+ $OPENSSL verify -purpose sslserver -CAfile <(cat $ADDITIONAL_CA_FILES "$bundle_fname") $HOSTCERT >$TEMPDIR/${certificate_file[i]}.1 2>$TEMPDIR/${certificate_file[i]}.2
+ fi)
+ verify_retcode[i]=$(awk '/error [1-9][0-9]? at [0-9]+ depth lookup:/ { if (!found) {print $2; found=1} }' $TEMPDIR/${certificate_file[i]}.1 $TEMPDIR/${certificate_file[i]}.2)
+ [[ -z "${verify_retcode[i]}" ]] && verify_retcode[i]=0
+ if [[ ${verify_retcode[i]} -eq 0 ]]; then
+ trust[i]=true
+ some_ok=true
+ [[ -z "$GOOD_CA_BUNDLE" ]] && GOOD_CA_BUNDLE="$bundle_fname"
+ debugme tm_svrty_good "Ok "
+ debugme tmln_out "${verify_retcode[i]}"
+ else
+ trust[i]=false
+ all_ok=false
+ debugme tm_svrty_high "not trusted "
+ debugme tmln_out "${verify_retcode[i]}"
+ fi
+ ((i++))
+ done
+ num_ca_bundles=$((i - 1))
+ debugme tm_out " "
+ if "$all_ok"; then
+ # all stores ok
+ pr_svrty_good "Ok "; pr_warning "$addtl_warning"
+ # we did to stdout the warning above already, so we could stay here with OK:
+ fileout "${jsonID}${json_postfix}" "OK" "passed. $addtl_warning"
+ else
+ # at least one failed
+ pr_svrty_critical "NOT ok"
+ if ! "$some_ok"; then
+ # all failed (we assume with the same issue), we're displaying the reason
+ out " "
+ code="$(verify_retcode_helper "${verify_retcode[1]}")"
+ if [[ "$code" =~ "pls report" ]]; then
+ pr_warning "$code"
+ else
+ out "$code"
+ fi
+ fileout "${jsonID}${json_postfix}" "CRITICAL" "failed $code. $addtl_warning"
+ else
+ # is one ok and the others not ==> display the culprit store
+ if "$some_ok"; then
+ pr_svrty_critical ":"
+ for ((i=1;i<=num_ca_bundles;i++)); do
+ if ${trust[i]}; then
+ ok_was="${certificate_file[i]} $ok_was"
+ else
+ #code="$(verify_retcode_helper ${verify_retcode[i]})"
+ #notok_was="${certificate_file[i]} $notok_was"
+ pr_svrty_high " ${certificate_file[i]} "
+ code="$(verify_retcode_helper "${verify_retcode[i]}")"
+ if [[ "$code" =~ "pls report" ]]; then
+ pr_warning "$code"
+ else
+ out "$code"
+ fi
+ notok_was="${certificate_file[i]} $code $notok_was"
+ fi
+ done
+ #pr_svrty_high "$notok_was "
+ #outln "$code"
+ outln
+ # lf + green ones
+ [[ "$DEBUG" -eq 0 ]] && tm_out "$spaces"
+ pr_svrty_good "OK: $ok_was"
+ fi
+ fileout "${jsonID}${json_postfix}" "CRITICAL" "Some certificate trust checks failed -> $notok_was $addtl_warning, OK -> $ok_was"
+ fi
+ [[ -n "$addtl_warning" ]] && out "\n$spaces" && pr_warning "$addtl_warning"
+ fi
+ outln
+ return 0
+}
+
+# not handled: Root CA supplied ("contains anchor" in SSLlabs terminology)
+
+tls_time() {
+ local difftime
+ local spaces=" "
+ local jsonID="TLS_timestamp"
+
+ pr_bold " TLS clock skew" ; out "$spaces"
+
+ if ( [[ "$STARTTLS_PROTOCOL" =~ ldap ]] || [[ "$STARTTLS_PROTOCOL" =~ irc ]] ); then
+ prln_local_problem "STARTTLS/$STARTTLS_PROTOCOL and --ssl-native collide here"
+ return 1
+ fi
+
+ TLS_DIFFTIME_SET=true # this is a switch whether we want to measure the remote TLS_TIME
+ tls_sockets "01" "$TLS_CIPHER" # try first TLS 1.0 (most frequently used protocol)
+ [[ -z "$TLS_TIME" ]] && tls_sockets "03" "$TLS12_CIPHER" # TLS 1.2
+ [[ -z "$TLS_TIME" ]] && tls_sockets "02" "$TLS_CIPHER" # TLS 1.1
+ [[ -z "$TLS_TIME" ]] && tls_sockets "00" "$TLS_CIPHER" # SSL 3
+
+ if [[ -n "$TLS_TIME" ]]; then # nothing returned a time!
+ difftime=$((TLS_TIME - TLS_NOW)) # TLS_NOW has been set in tls_sockets()
+ if [[ "${#difftime}" -gt 5 ]]; then
+ # openssl >= 1.0.1f fills this field with random values! --> good for possible fingerprint
+ out "Random values, no fingerprinting possible "
+ fileout "$jsonID" "INFO" "random"
+ else
+ [[ $difftime != "-"* ]] && [[ $difftime != "0" ]] && difftime="+$difftime"
+ out "$difftime"; out " sec from localtime";
+ fileout "$jsonID" "INFO" "off by $difftime seconds from your localtime"
+ fi
+ debugme tm_out "$TLS_TIME"
+ outln
+ else
+ outln "SSLv3 through TLS 1.2 didn't return a timestamp"
+ fileout "$jsonID" "INFO" "None returned by SSLv3 through TLSv1.2"
+ fi
+ TLS_DIFFTIME_SET=false # reset the switch to save calls to date and friend in tls_sockets()
+ return 0
+}
+
+# core function determining whether handshake succeeded or not
+# arg1: return value of "openssl s_client connect"
+# arg2: temporary file with the server hello
+# returns 0 if connect was successful, 1 if not
+#
+sclient_connect_successful() {
+ local server_hello="$(cat -v "$2")"
+ local re='Master-Key: ([^\
+]*)'
+
+ [[ $1 -eq 0 ]] && return 0
+ if [[ "$server_hello" =~ $re ]]; then
+ [[ -n "${BASH_REMATCH[1]}" ]] && return 0
+ fi
+ [[ "$server_hello" =~ (New|Reused)", "(SSLv[23]|TLSv1(\.[0-3])?(\/SSLv3)?)", Cipher is "([A-Z0-9]+-[A-Za-z0-9\-]+|TLS_[A-Za-z0-9_]+) ]] && return 0
+ # what's left now is: master key empty and Session-ID not empty
+ # ==> probably client-based auth with x509 certificate. We handle that at other places
+ #
+ # For robustness we also detected here network / server connectivity problems:
+ # Just need to check whether $TMPFILE=$2 is empty
+ if [[ ! -s "$2" ]]; then
+ ((NR_OSSL_FAIL++))
+ connectivity_problem $NR_OSSL_FAIL $MAX_OSSL_FAIL "openssl s_client connect problem" "repeated openssl s_client connect problem, doesn't make sense to continue"
+ fi
+ return 1
+}
+
+extract_new_tls_extensions() {
+ local tls_extensions
+
+ # this is not beautiful (grep+sed)
+ # but maybe we should just get the ids and do a private matching, according to
+ # https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml
+ tls_extensions=$(grep -a 'TLS server extension ' "$1" | \
+ sed -e 's/TLS server extension //g' -e 's/\" (id=/\/#/g' \
+ -e 's/,.*$/,/g' -e 's/),$/\"/g' \
+ -e 's/elliptic curves\/#10/supported_groups\/#10/g')
+ tls_extensions=$(echo $tls_extensions) # into one line
+
+ if [[ -n "$tls_extensions" ]]; then
+ # check to see if any new TLS extensions were returned and add any new ones to TLS_EXTENSIONS
+ while read -d "\"" -r line; do
+ if [[ $line != "" ]] && [[ ! "$TLS_EXTENSIONS" =~ "$line" ]]; then
+#FIXME: This is a string of quoted strings, so this seems to determine the output format already. Better e.g. would be an array
+ TLS_EXTENSIONS+=" \"${line}\""
+ fi
+ done <<<$tls_extensions
+ [[ "${TLS_EXTENSIONS:0:1}" == " " ]] && TLS_EXTENSIONS="${TLS_EXTENSIONS:1}"
+ fi
+}
+
+# Note that since, at the moment, this function is only called by run_server_defaults()
+# and run_heartbleed(), this function does not look for the status request or NPN
+# extensions. For run_heartbleed(), only the heartbeat extension needs to be detected.
+# For run_server_defaults(), the status request and NPN would already be detected by
+# get_server_certificate(), if they are supported. In the case of the status extension,
+# since including a status request extension in a ClientHello does not work for GOST
+# only servers. In the case of NPN, since a server will not include both the NPN and
+# ALPN extensions in the same ServerHello.
+#
+determine_tls_extensions() {
+ local addcmd
+ local -i success=1
+ local line params="" tls_extensions=""
+ local alpn_proto alpn="" alpn_list_len_hex alpn_extn_len_hex
+ local -i alpn_list_len alpn_extn_len
+ local cbc_cipher_list="ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA256:DH-RSA-AES256-SHA256:DH-DSS-AES256-SHA256:DHE-RSA-AES256-SHA:DHE-DSS-AES256-SHA:DH-RSA-AES256-SHA:DH-DSS-AES256-SHA:ECDHE-RSA-CAMELLIA256-SHA384:ECDHE-ECDSA-CAMELLIA256-SHA384:DHE-RSA-CAMELLIA256-SHA256:DHE-DSS-CAMELLIA256-SHA256:DH-RSA-CAMELLIA256-SHA256:DH-DSS-CAMELLIA256-SHA256:DHE-RSA-CAMELLIA256-SHA:DHE-DSS-CAMELLIA256-SHA:DH-RSA-CAMELLIA256-SHA:DH-DSS-CAMELLIA256-SHA:ECDH-RSA-AES256-SHA384:ECDH-ECDSA-AES256-SHA384:ECDH-RSA-AES256-SHA:ECDH-ECDSA-AES256-SHA:ECDH-RSA-CAMELLIA256-SHA384:ECDH-ECDSA-CAMELLIA256-SHA384:AES256-SHA256:AES256-SHA:CAMELLIA256-SHA256:CAMELLIA256-SHA:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:DHE-RSA-AES128-SHA256:DHE-DSS-AES128-SHA256:DH-RSA-AES128-SHA256:DH-DSS-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA:DH-RSA-AES128-SHA:DH-DSS-AES128-SHA:ECDHE-RSA-CAMELLIA128-SHA256:ECDHE-ECDSA-CAMELLIA128-SHA256:DHE-RSA-CAMELLIA128-SHA256:DHE-DSS-CAMELLIA128-SHA256:DH-RSA-CAMELLIA128-SHA256:DH-DSS-CAMELLIA128-SHA256:DHE-RSA-SEED-SHA:DHE-DSS-SEED-SHA:DH-RSA-SEED-SHA:DH-DSS-SEED-SHA:DHE-RSA-CAMELLIA128-SHA:DHE-DSS-CAMELLIA128-SHA:DH-RSA-CAMELLIA128-SHA:DH-DSS-CAMELLIA128-SHA:ECDH-RSA-AES128-SHA256:ECDH-ECDSA-AES128-SHA256:ECDH-RSA-AES128-SHA:ECDH-ECDSA-AES128-SHA:ECDH-RSA-CAMELLIA128-SHA256:ECDH-ECDSA-CAMELLIA128-SHA256:AES128-SHA256:AES128-SHA:CAMELLIA128-SHA256:SEED-SHA:CAMELLIA128-SHA:IDEA-CBC-SHA:ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:EDH-DSS-DES-CBC3-SHA:DH-RSA-DES-CBC3-SHA:DH-DSS-DES-CBC3-SHA:ECDH-RSA-DES-CBC3-SHA:ECDH-ECDSA-DES-CBC3-SHA:DES-CBC3-SHA:EXP1024-DHE-DSS-DES-CBC-SHA:EDH-RSA-DES-CBC-SHA:EDH-DSS-DES-CBC-SHA:DH-RSA-DES-CBC-SHA:DH-DSS-DES-CBC-SHA:EXP1024-DES-CBC-SHA:DES-CBC-SHA:EXP-EDH-RSA-DES-CBC-SHA:EXP-EDH-DSS-DES-CBC-SHA:EXP-DES-CBC-SHA:EXP-RC2-CBC-MD5:EXP-DH-DSS-DES-CBC-SHA:EXP-DH-RSA-DES-CBC-SHA"
+ local cbc_cipher_list_hex="c0,28, c0,24, c0,14, c0,0a, 00,6b, 00,6a, 00,69, 00,68, 00,39, 00,38, 00,37, 00,36, c0,77, c0,73, 00,c4, 00,c3, 00,c2, 00,c1, 00,88, 00,87, 00,86, 00,85, c0,2a, c0,26, c0,0f, c0,05, c0,79, c0,75, 00,3d, 00,35, 00,c0, 00,84, c0,3d, c0,3f, c0,41, c0,43, c0,45, c0,49, c0,4b, c0,4d, c0,4f, c0,27, c0,23, c0,13, c0,09, 00,67, 00,40, 00,3f, 00,3e, 00,33, 00,32, 00,31, 00,30, c0,76, c0,72, 00,be, 00,bd, 00,bc, 00,bb, 00,9a, 00,99, 00,98, 00,97, 00,45, 00,44, 00,43, 00,42, c0,29, c0,25, c0,0e, c0,04, c0,78, c0,74, 00,3c, 00,2f, 00,ba, 00,96, 00,41, 00,07, c0,3c, c0,3e, c0,40, c0,42, c0,44, c0,48, c0,4a, c0,4c, c0,4e, c0,12, c0,08, 00,16, 00,13, 00,10, 00,0d, c0,0d, c0,03, 00,0a, fe,ff, ff,e0, 00,63, 00,15, 00,12, 00,0f, 00,0c, 00,62, 00,09, fe,fe, ff,e1, 00,14, 00,11, 00,08, 00,06, 00,0b, 00,0e"
+ local using_sockets=true
+
+ [[ "$OPTIMAL_PROTO" == -ssl2 ]] && return 0
+ "$SSL_NATIVE" && using_sockets=false
+
+ if "$using_sockets"; then
+ tls_extensions="00,01,00,01,02, 00,02,00,00, 00,04,00,00, 00,12,00,00, 00,16,00,00, 00,17,00,00"
+ if [[ -z $STARTTLS ]]; then
+ for alpn_proto in $ALPN_PROTOs; do
+ alpn+=",$(printf "%02x" ${#alpn_proto}),$(string_to_asciihex "$alpn_proto")"
+ done
+ alpn_list_len=${#alpn}/3
+ alpn_list_len_hex=$(printf "%04x" $alpn_list_len)
+ alpn_extn_len=$alpn_list_len+2
+ alpn_extn_len_hex=$(printf "%04x" $alpn_extn_len)
+ tls_extensions+=", 00,10,${alpn_extn_len_hex:0:2},${alpn_extn_len_hex:2:2},${alpn_list_len_hex:0:2},${alpn_list_len_hex:2:2}$alpn"
+ fi
+ if [[ ! "$TLS_EXTENSIONS" =~ encrypt-then-mac ]]; then
+ tls_sockets "03" "$cbc_cipher_list_hex, 00,ff" "all" "$tls_extensions"
+ success=$?
+ fi
+ if [[ $success -ne 0 ]] && [[ $success -ne 2 ]]; then
+ tls_sockets "03" "$TLS12_CIPHER" "all" "$tls_extensions"
+ success=$?
+ fi
+ [[ $success -eq 2 ]] && success=0
+ [[ $success -eq 0 ]] && extract_new_tls_extensions "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt"
+ if [[ -r "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt" ]]; then
+ cp "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt" $TMPFILE
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ fi
+ else
+ if "$HAS_ALPN" && [[ -z $STARTTLS ]]; then
+ params="-alpn \"${ALPN_PROTOs// /,}\"" # we need to replace " " by ","
+ elif "$HAS_NPN" && [[ -z $STARTTLS ]]; then
+ params="-nextprotoneg \"$NPN_PROTOs\""
+ fi
+ if [[ -z "$OPTIMAL_PROTO" ]] && [[ -z "$SNI" ]] && "$HAS_NO_SSL2"; then
+ addcmd="-no_ssl2"
+ else
+ addcmd="$SNI"
+ fi
+ if [[ ! "$TLS_EXTENSIONS" =~ encrypt-then-mac ]]; then
+ $OPENSSL s_client $(s_client_options "$STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY $addcmd $OPTIMAL_PROTO -tlsextdebug $params -cipher $cbc_cipher_list") </dev/null 2>$ERRFILE >$TMPFILE
+ sclient_connect_successful $? $TMPFILE
+ success=$?
+ fi
+ if [[ $success -ne 0 ]]; then
+ $OPENSSL s_client $(s_client_options "$STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY $addcmd $OPTIMAL_PROTO -tlsextdebug $params") </dev/null 2>$ERRFILE >$TMPFILE
+ sclient_connect_successful $? $TMPFILE
+ success=$?
+ fi
+ [[ $success -eq 0 ]] && extract_new_tls_extensions $TMPFILE
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ fi
+ return $success
+}
+
+extract_certificates() {
+ local version="$1"
+ local savedir
+ local -i i success nrsaved=0
+ local issuerDN CAsubjectDN previssuerDN
+
+ # Place the server's certificate in $HOSTCERT and any intermediate
+ # certificates that were provided in $TEMPDIR/intermediatecerts.pem
+ savedir="$PWD"; cd $TEMPDIR
+ # https://backreference.org/2010/05/09/ocsp-verification-with-openssl/
+ if [[ "$version" == ssl2 ]]; then
+ awk -v n=-1 '/Server certificate/ {start=1}
+ /-----BEGIN CERTIFICATE-----/{ if (start) {inc=1; n++} }
+ inc { print > ("level" n ".crt") }
+ /---END CERTIFICATE-----/{ inc=0 }' $TMPFILE
+ else
+ awk -v n=-1 '/Certificate chain/ {start=1}
+ /-----BEGIN CERTIFICATE-----/{ if (start) {inc=1; n++} }
+ inc { print > ("level" n ".crt") }
+ /---END CERTIFICATE-----/{ inc=0 }' $TMPFILE
+ fi
+ [[ -s level0.crt ]] && nrsaved=$(count_words "$(echo level?.crt 2>/dev/null)")
+ if [[ $nrsaved -eq 0 ]]; then
+ success=1
+ else
+ success=0
+ CERTIFICATE_LIST_ORDERING_PROBLEM=false
+ mv level0.crt $HOSTCERT
+ if [[ $nrsaved -eq 1 ]]; then
+ echo "" > $TEMPDIR/intermediatecerts.pem
+ else
+ cat level?.crt > $TEMPDIR/intermediatecerts.pem
+ issuerDN="$($OPENSSL x509 -in $HOSTCERT -noout -issuer 2>/dev/null)"
+ issuerDN="${issuerDN:8}"
+ previssuerDN="$issuerDN"
+ # The second certificate (level1.crt) SHOULD be issued to the CA
+ # that issued the server's certificate. But, according to RFC 8446
+ # clients SHOULD be prepared to handle cases in which the server
+ # does not order the certificates correctly.
+ for (( i=1; i < nrsaved; i++ )); do
+ CAsubjectDN="$($OPENSSL x509 -in "level$i.crt" -noout -subject 2>/dev/null)"
+ if [[ "${CAsubjectDN:9}" == "$issuerDN" ]]; then
+ cp "level$i.crt" $TEMPDIR/hostcert_issuer.pem
+ issuerDN="" # set to empty to prevent further matches
+ fi
+ [[ "${CAsubjectDN:9}" != "$previssuerDN" ]] && CERTIFICATE_LIST_ORDERING_PROBLEM=true
+ "$CERTIFICATE_LIST_ORDERING_PROBLEM" && [[ -z "$issuerDN" ]] && break
+ previssuerDN="$($OPENSSL x509 -in "level$i.crt" -noout -issuer 2>/dev/null)"
+ previssuerDN="${previssuerDN:8}"
+ done
+ # This should never happen, but if more than one certificate was
+ # provided and none of them belong to the CA that issued the
+ # server's certificate, then the extra certificates should just
+ # be deleted. There is code elsewhere that assumes that if
+ # $TEMPDIR/intermediatecerts.pem is non-empty, then
+ # $TEMPDIR/hostcert_issuer.pem is also present.
+ [[ -n "$issuerDN" ]] && echo "" > $TEMPDIR/intermediatecerts.pem
+ rm level?.crt
+ fi
+ fi
+ cd "$savedir"
+ return $success
+}
+
+extract_stapled_ocsp() {
+ local response="$(cat $TMPFILE)"
+ local ocsp tmp
+ local -i ocsp_len
+
+ STAPLED_OCSP_RESPONSE=""
+ if [[ "$response" =~ CertificateStatus ]]; then
+ # This is OpenSSL 1.1.0 or 1.1.1 and the response
+ # is TLS 1.2 or earlier.
+ ocsp="${response##*CertificateStatus}"
+ ocsp="16${ocsp#*16}"
+ ocsp="${ocsp%%<<<*}"
+ ocsp="$(strip_spaces "$(newline_to_spaces "$ocsp")")"
+ ocsp="${ocsp:8}"
+ elif [[ "$response" =~ "TLS server extension \"status request\" (id=5), len=0" ]]; then
+ # This is not OpenSSL 1.1.0 or 1.1.1, and the response
+ # is TLS 1.2 or earlier.
+ ocsp="${response%%OCSP response:*}"
+ ocsp="${ocsp##*<<<}"
+ ocsp="16${ocsp#*16}"
+ ocsp="$(strip_spaces "$(newline_to_spaces "$ocsp")")"
+ ocsp="${ocsp:8}"
+ elif [[ "$response" =~ "TLS server extension \"status request\" (id=5), len=" ]]; then
+ # This is OpenSSL 1.1.1 and the response is TLS 1.3.
+ ocsp="${response##*TLS server extension \"status request\" (id=5), len=}"
+ ocsp="${ocsp%%<<<*}"
+ tmp="${ocsp%%[!0-9]*}"
+ ocsp="${ocsp#$tmp}"
+ ocsp_len=2*$tmp
+ ocsp="$(awk ' { print $3 $4 $5 $6 $7 $8 $9 $10 $11 $12 $13 $14 $15 $16 $17 } ' <<< "$ocsp" | sed 's/-//')"
+ ocsp="$(strip_spaces "$(newline_to_spaces "$ocsp")")"
+ ocsp="${ocsp:0:ocsp_len}"
+ else
+ return 0
+ fi
+ # Determine whether this is a single OCSP response or a sequence of
+ # responses and then extract just the response for the server's
+ # certificate.
+ if [[ "${ocsp:0:2}" == "01" ]]; then
+ STAPLED_OCSP_RESPONSE="${ocsp:8}"
+ elif [[ "${ocsp:0:2}" == "02" ]]; then
+ ocsp_len=2*$(hex2dec "${tls_certificate_status_ascii:8:6}")
+ STAPLED_OCSP_RESPONSE="${ocsp:14:ocsp_len}"
+ fi
+ return 0
+}
+
+# arg1 is "<OpenSSL cipher>"
+# arg2 is a list of protocols to try (tls1_2, tls1_1, tls1, ssl3) or empty (if all should be tried)
+get_server_certificate() {
+ local protocols_to_try proto
+ local success ret
+ local npn_params="" line
+ local ciphers_to_test=""
+ # Cipher suites that use a certificate with an RSA (signature) public key
+ local -r a_rsa="cc,13, cc,15, c0,30, c0,28, c0,14, 00,9f, cc,a8, cc,aa, c0,a3, c0,9f, 00,6b, 00,39, c0,77, 00,c4, 00,88, c0,45, c0,4d, c0,53, c0,61, c0,7d, c0,8b, 16,b7, 16,b9, c0,2f, c0,27, c0,13, 00,9e, c0,a2, c0,9e, 00,67, 00,33, c0,76, 00,be, 00,9a, 00,45, c0,44, c0,4c, c0,52, c0,60, c0,7c, c0,8a, c0,11, c0,12, 00,16, 00,15, 00,14, c0,10"
+ # Cipher suites that use a certificate with an RSA (encryption) public key
+ local -r e_rsa="00,b7, c0,99, 00,ad, cc,ae, 00,9d, c0,a1, c0,9d, 00,3d, 00,35, 00,c0, 00,84, 00,95, c0,3d, c0,51, c0,69, c0,6f, c0,7b, c0,93, ff,01, 00,ac, c0,a0, c0,9c, 00,9c, 00,3c, 00,2f, 00,ba, 00,b6, 00,96, 00,41, c0,98, 00,07, 00,94, c0,3c, c0,50, c0,68, c0,6e, c0,7a, c0,92, 00,05, 00,04, 00,92, 00,0a, 00,93, fe,ff, ff,e0, 00,62, 00,09, 00,61, fe,fe, ff,e1, 00,64, 00,60, 00,08, 00,06, 00,03, 00,b9, 00,b8, 00,2e, 00,3b, 00,02, 00,01, ff,00"
+ # Cipher suites that use a certificate with a DSA public key
+ local -r a_dss="00,a3, 00,6a, 00,38, 00,c3, 00,87, c0,43, c0,57, c0,81, 00,a2, 00,40, 00,32, 00,bd, 00,99, 00,44, c0,42, c0,56, c0,80, 00,66, 00,13, 00,63, 00,12, 00,65, 00,11"
+ # Cipher suites that use a certificate with a DH public key
+ local -r a_dh="00,a5, 00,a1, 00,69, 00,68, 00,37, 00,36, 00,c2, 00,c1, 00,86, 00,85, c0,3f, c0,41, c0,55, c0,59, c0,7f, c0,83, 00,a4, 00,a0, 00,3f, 00,3e, 00,31, 00,30, 00,bc, 00,bb, 00,98, 00,97, 00,43, 00,42, c0,3e, c0,40, c0,54, c0,58, c0,7e, c0,82, 00,10, 00,0d, 00,0f, 00,0c, 00,0b, 00,0e"
+ # Cipher suites that use a certificate with an ECDH public key
+ local -r a_ecdh="c0,32, c0,2e, c0,2a, c0,26, c0,0f, c0,05, c0,79, c0,75, c0,4b, c0,4f, c0,5f, c0,63, c0,89, c0,8d, c0,31, c0,2d, c0,29, c0,25, c0,0e, c0,04, c0,78, c0,74, c0,4a, c0,4e, c0,5e, c0,62, c0,88, c0,8c, c0,0c, c0,02, c0,0d, c0,03, c0,0b, c0,01"
+ # Cipher suites that use a certificate with an ECDSA public key
+ local -r a_ecdsa="cc,14, c0,2c, c0,24, c0,0a, cc,a9, c0,af, c0,ad, c0,73, c0,49, c0,5d, c0,87, 16,b8, 16,ba, c0,2b, c0,23, c0,09, c0,ae, c0,ac, c0,72, c0,48, c0,5c, c0,86, c0,07, c0,08, c0,06"
+ # Cipher suites that use a certificate with a GOST public key
+ local -r a_gost="00,80, 00,81, 00,82, 00,83"
+ local using_sockets=true
+
+ "$SSL_NATIVE" && using_sockets=false
+
+ CERTIFICATE_LIST_ORDERING_PROBLEM=false
+ if [[ "$1" =~ "tls1_3" ]]; then
+ [[ $(has_server_protocol "tls1_3") -eq 1 ]] && return 1
+ if "$HAS_TLS13"; then
+ if [[ "$1" =~ "tls1_3_RSA" ]]; then
+ $OPENSSL s_client $(s_client_options "$STARTTLS $BUGS -showcerts -connect $NODEIP:$PORT $PROXY $SNI -tls1_3 -tlsextdebug -status -msg -sigalgs PSS+SHA256:PSS+SHA384") </dev/null 2>$ERRFILE >$TMPFILE
+ elif [[ "$1" =~ "tls1_3_ECDSA" ]]; then
+ $OPENSSL s_client $(s_client_options "$STARTTLS $BUGS -showcerts -connect $NODEIP:$PORT $PROXY $SNI -tls1_3 -tlsextdebug -status -msg -sigalgs ECDSA+SHA256:ECDSA+SHA384") </dev/null 2>$ERRFILE >$TMPFILE
+ else
+ return 1
+ fi
+ sclient_connect_successful $? $TMPFILE || return 1
+ DETECTED_TLS_VERSION="0304"
+ extract_certificates "tls1_3"
+ extract_stapled_ocsp
+ success=$?
+ else
+ # For STARTTLS protocols not being implemented yet via sockets this is a bypass otherwise it won't be usable at all (e.g. LDAP)
+ if ( [[ "$STARTTLS" =~ ldap ]] || [[ "$STARTTLS" =~ irc ]] ); then
+ return 1
+ elif [[ "$1" =~ "tls1_3_RSA" ]]; then
+ tls_sockets "04" "$TLS13_CIPHER" "all+" "00,12,00,00, 00,05,00,05,01,00,00,00,00, 00,0d,00,10,00,0e,08,04,08,05,08,06,04,01,05,01,06,01,02,01"
+ elif [[ "$1" =~ "tls1_3_ECDSA" ]]; then
+ tls_sockets "04" "$TLS13_CIPHER" "all+" "00,12,00,00, 00,05,00,05,01,00,00,00,00, 00,0d,00,0a,00,08,04,03,05,03,06,03,02,03"
+ else
+ return 1
+ fi
+ success=$?
+ [[ $success -eq 0 ]] || return 1
+ cp "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt" $TMPFILE
+ fi
+ [[ $success -eq 0 ]] && add_tls_offered tls1_3 yes
+ extract_new_tls_extensions $TMPFILE
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return $success
+ fi
+
+ "$HAS_NPN" && [[ -z "$STARTTLS" ]] && npn_params="-nextprotoneg \"$NPN_PROTOs\""
+
+ if [[ -n "$2" ]]; then
+ protocols_to_try="$2"
+ else
+ protocols_to_try="tls1_2 tls1_1 tls1 ssl3"
+ fi
+
+ # throwing 1st every cipher/protocol at the server to know what works
+ success=7
+
+ if [[ "$OPTIMAL_PROTO" == -ssl2 ]]; then
+ success=1
+ sslv2_sockets "" "true"
+ if [[ $? -eq 3 ]]; then
+ mv $TEMPDIR/$NODEIP.parse_sslv2_serverhello.txt $TMPFILE
+ success=0
+ fi
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return $success
+ fi
+
+ if "$using_sockets"; then
+ protocols_to_try="${protocols_to_try/tls1_2/03}"
+ protocols_to_try="${protocols_to_try/tls1_1/02}"
+ protocols_to_try="${protocols_to_try/tls1/01}"
+ protocols_to_try="${protocols_to_try/ssl3/00}"
+
+ [[ "$1" =~ aRSA ]] && ciphers_to_test+=", $a_rsa"
+ [[ "$1" =~ eRSA ]] && ciphers_to_test+=", $e_rsa"
+ [[ "$1" =~ aDSS ]] && ciphers_to_test+=", $a_dss"
+ [[ "$1" =~ aDH ]] && ciphers_to_test+=", $a_dh"
+ [[ "$1" =~ aECDH ]] && ciphers_to_test+=", $a_ecdh"
+ [[ "$1" =~ aECDSA ]] && ciphers_to_test+=", $a_ecdsa"
+ [[ "$1" =~ aGOST ]] && ciphers_to_test+=", $a_gost"
+
+ [[ -z "$ciphers_to_test" ]] && return 1
+ ciphers_to_test="${ciphers_to_test:2}"
+
+ for proto in $protocols_to_try; do
+ [[ 1 -eq $(has_server_protocol $proto) ]] && continue
+ tls_sockets "$proto" "$ciphers_to_test, 00,ff" "all" "00,12,00,00, 00,05,00,05,01,00,00,00,00"
+ ret=$?
+ [[ $ret -eq 0 ]] && success=0 && break
+ [[ $ret -eq 2 ]] && success=0 && break
+ done # this loop is needed for IIS6 and others which have a handshake size limitations
+ if [[ $success -eq 7 ]]; then
+ # "-status" above doesn't work for GOST only servers, so we do another test without it and see whether that works then:
+ tls_sockets "$proto" "$ciphers_to_test, 00,ff" "all" "00,12,00,00"
+ ret=$?
+ [[ $ret -eq 0 ]] && success=0
+ [[ $ret -eq 2 ]] && success=0
+ if [[ $success -eq 7 ]]; then
+ if [ -z "$1" ]; then
+ prln_warning "Strange, no SSL/TLS protocol seems to be supported (error around line $((LINENO - 6)))"
+ fi
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 7 # this is ugly, I know
+ else
+ GOST_STATUS_PROBLEM=true
+ fi
+ fi
+ cp $TEMPDIR/$NODEIP.parse_tls_serverhello.txt $TMPFILE
+
+ # When "$2" is empty, get_server_certificate() is being called with SNI="".
+ # In case the extensions returned by the server differ depending on whether
+ # SNI is provided or not, don't collect extensions when SNI="" (unless
+ # no DNS name was provided at the command line).
+ [[ -z "$2" ]] && extract_new_tls_extensions $TMPFILE
+ else
+ # no sockets, openssl
+ ciphers_to_test="$1"
+ if [[ "$1" =~ aRSA ]] && [[ "$1" =~ eRSA ]]; then
+ ciphers_to_test="${ciphers_to_test/eRSA/}"
+ elif [[ "$1" =~ aRSA ]]; then
+ ciphers_to_test="${ciphers_to_test/aRSA/}"
+ for ciph in $(colon_to_spaces $(actually_supported_osslciphers "aRSA")); do
+ [[ "$ciph" =~ -RSA- ]] && ciphers_to_test+=":$ciph"
+ done
+ elif [[ "$1" =~ eRSA ]]; then
+ ciphers_to_test="${ciphers_to_test/eRSA/}"
+ for ciph in $(colon_to_spaces $(actually_supported_osslciphers "aRSA")); do
+ [[ ! "$ciph" =~ -RSA- ]] && ciphers_to_test+=":$ciph"
+ done
+ fi
+ ciphers_to_test="${ciphers_to_test/::/:}"
+ [[ "${ciphers_to_test:0:1}" == : ]] && ciphers_to_test="${ciphers_to_test:1}"
+ [[ $(count_ciphers $(actually_supported_osslciphers "$ciphers_to_test")) -ge 1 ]] || return 1
+
+ for proto in $protocols_to_try; do
+ [[ 1 -eq $(has_server_protocol $proto) ]] && continue
+ [[ "$proto" == ssl3 ]] && ! "$HAS_SSL3" && continue
+ addcmd=""
+ $OPENSSL s_client $(s_client_options "$STARTTLS $BUGS -cipher $ciphers_to_test -showcerts -connect $NODEIP:$PORT $PROXY $SNI -$proto -tlsextdebug $npn_params -status -msg") </dev/null 2>$ERRFILE >$TMPFILE
+ if sclient_connect_successful $? $TMPFILE; then
+ success=0
+ break # now we have the certificate
+ fi
+ done # this loop is needed for IIS6 and others which have a handshake size limitations
+ if [[ $success -eq 7 ]]; then
+ # "-status" above doesn't work for GOST only servers, so we do another test without it and see whether that works then:
+ [[ "$proto" == ssl3 ]] && ! "$HAS_SSL3" && return 7
+ $OPENSSL s_client $(s_client_options "$STARTTLS $BUGS -cipher $ciphers_to_test -showcerts -connect $NODEIP:$PORT $PROXY $SNI -$proto -tlsextdebug") </dev/null 2>>$ERRFILE >$TMPFILE
+ if ! sclient_connect_successful $? $TMPFILE; then
+ if [ -z "$1" ]; then
+ prln_warning "Strange, no SSL/TLS protocol seems to be supported (error around line $((LINENO - 6)))"
+ fi
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 7 # this is ugly, I know
+ else
+ GOST_STATUS_PROBLEM=true
+ fi
+ fi
+ case "$proto" in
+ "tls1_2") DETECTED_TLS_VERSION="0303" ;;
+ "tls1_1") DETECTED_TLS_VERSION="0302" ;;
+ "tls1") DETECTED_TLS_VERSION="0301" ;;
+ "ssl3") DETECTED_TLS_VERSION="0300" ;;
+ esac
+ # When "$2" is empty, get_server_certificate() is being called with SNI="".
+ # In case the extensions returned by the server differ depending on whether
+ # SNI is provided or not, don't collect extensions when SNI="" (unless
+ # no DNS name was provided at the command line).
+ [[ -z "$2" ]] && extract_new_tls_extensions $TMPFILE
+
+ extract_certificates "$proto"
+ extract_stapled_ocsp
+ success=$?
+ fi
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return $success
+}
+
+# arg1: path to certificate
+# returns CN
+get_cn_from_cert() {
+ local subject
+
+ # attention! openssl 1.0.2 doesn't properly handle online output from certificates from trustwave.com/github.com
+ #FIXME: use -nameopt oid for robustness
+
+ # for e.g. russian sites -esc_msb,utf8 works in an UTF8 terminal -- any way to check platform independent?
+ # see x509(1ssl):
+ subject="$($OPENSSL x509 -in $1 -noout -subject -nameopt multiline,-align,sname,-esc_msb,utf8,-space_eq 2>>$ERRFILE)"
+ echo "$(awk -F'=' '/CN=/ { print $2 }' <<< "$subject" | tr '\n' ' ')"
+ return $?
+}
+
+# Return 0 if the name provided in arg1 is a wildcard name
+is_wildcard()
+{
+ local certname="$1"
+
+ # If the first label in the DNS name begins "xn--", then assume it is an
+ # A-label and not a wildcard name (RFC 6125, Section 6.4.3).
+ [[ "${certname:0:4}" == "xn--" ]] && return 1
+
+ # Remove part of name preceding '*' or '.'. If no "*" appears in the
+ # left-most label, then it is not a wildcard name (RFC 6125, Section 6.4.3).
+ basename="$(echo -n "$certname" | sed 's/^[_a-zA-Z0-9\-]*//')"
+ [[ "${basename:0:1}" != "*" ]] && return 1 # not a wildcard name
+
+ # Check that there are no additional wildcard ('*') characters or any
+ # other characters that do not belong in a DNS name.
+ [[ -n $(echo -n "${basename:1}" | sed 's/^[_\.a-zA-Z0-9\-]*//') ]] && return 1
+ return 0
+}
+
+# Return 0 if the name provided in arg2 is a wildcard name and it matches the name provided in arg1.
+wildcard_match()
+{
+ local servername="$1"
+ local certname="$2"
+ local basename
+ local -i basename_offset len_certname len_part1 len_basename
+ local -i len_servername len_wildcard
+
+ len_servername=${#servername}
+ len_certname=${#certname}
+
+ # Use rules from RFC 6125 to perform the match.
+
+ # Assume the "*" in the wildcard needs to be replaced by one or more
+ # characters, although RFC 6125 is not clear about that.
+ [[ $len_servername -lt $len_certname ]] && return 1
+
+ is_wildcard "$certname"
+ [[ $? -ne 0 ]] && return 1
+
+ # Comparisons of DNS names are case insensitive, so convert both names to uppercase.
+ certname="$(toupper "$certname")"
+ servername="$(toupper "$servername")"
+
+ # Extract part of name that comes after the "*"
+ basename="$(echo -n "$certname" | sed 's/^[_A-Z0-9\-]*\*//')"
+ len_basename=${#basename}
+ len_part1=$len_certname-$len_basename-1
+ len_wildcard=$len_servername-$len_certname+1
+ basename_offset=$len_servername-$len_basename
+
+ # Check that initial part of $servername matches initial part of $certname
+ # and that final part of $servername matches final part of $certname.
+ [[ "${servername:0:len_part1}" != "${certname:0:len_part1}" ]] && return 1
+ [[ "${servername:basename_offset:len_basename}" != "$basename" ]] && return 1
+
+ # Check that part of $servername that matches "*" is all part of a single
+ # domain label.
+ [[ -n $(echo -n "${servername:len_part1:len_wildcard}" | sed 's/^[_A-Z0-9\-]*//') ]] && return 1
+
+ return 0
+}
+
+# Compare the server name provided in arg1 to the CN and SAN in arg2 and return:
+# 0, if server name provided does not match any of the names in the CN or SAN
+# 1, if the server name provided matches a name in the SAN
+# 2, if the server name provided is a wildcard match against a name in the SAN
+# 4, if the server name provided matches the CN
+# 5, if the server name provided matches the CN AND a name in the SAN
+# 6, if the server name provided matches the CN AND is a wildcard match against a name in the SAN
+# 8, if the server name provided is a wildcard match against the CN
+# 9, if the server name provided matches a name in the SAN AND is a wildcard match against the CN
+# 10, if the server name provided is a wildcard match against the CN AND a name in the SAN
+
+compare_server_name_to_cert() {
+ local cert="$1"
+ local servername cn dns_sans ip_sans san dercert tag
+ local srv_id="" xmppaddr=""
+ local -i i len len1
+ local -i subret=0 # no error condition, passing results
+
+ HAS_DNS_SANS=false
+ if [[ -n "$XMPP_HOST" ]]; then
+ # RFC 6120, Section 13.7.2.1, states that for XMPP the identity that
+ # should appear in the server's certificate is identity that appears
+ # in the the 'to' address that the client communicates in the initial
+ # stream header.
+ servername="$(toupper "$XMPP_HOST")"
+ else
+ servername="$(toupper "$NODE")"
+ fi
+
+ # Check whether any of the DNS names in the certificate match the servername
+ dns_sans="$(get_san_dns_from_cert "$cert")"
+ while read san; do
+ if [[ -n "$san" ]]; then
+ HAS_DNS_SANS=true
+ [[ $(toupper "$san") == "$servername" ]] && subret=1 && break
+ fi
+ done <<< "$dns_sans"
+
+ if [[ $subret -eq 0 ]]; then
+ # Check whether any of the IP addresses in the certificate match the servername
+ ip_sans=$($OPENSSL x509 -in "$cert" -noout -text 2>>$ERRFILE | grep -A2 "Subject Alternative Name" | \
+ tr ',' '\n' | grep "IP Address:" | sed -e 's/IP Address://g' -e 's/ //g')
+ while read san; do
+ [[ -n "$san" ]] && [[ "$san" == "$servername" ]] && subret=1 && break
+ done <<< "$ip_sans"
+ fi
+
+ if [[ $subret -eq 0 ]] && [[ -n "$XMPP_HOST" ]]; then
+ # For XMPP hosts, in addition to checking for a matching DNS name,
+ # should also check for a matching SRV-ID or XmppAddr identifier.
+ dercert="$($OPENSSL x509 -in "$cert" -outform DER 2>>$ERRFILE | hexdump -v -e '16/1 "%02X"')"
+ # Look for the beginning of the subjectAltName extension. It
+ # will begin with the OID (2.5.29.17 = 0603551D11). After the OID
+ # there may be an indication that the extension is critical (0101FF).
+ # Finally will be the tag indicating that the value of the extension is
+ # encoded as an OCTET STRING (04).
+ if [[ "$dercert" =~ 0603551D110101FF04 ]]; then
+ dercert="${dercert##*0603551D110101FF04}"
+ else
+ dercert="${dercert##*0603551D1104}"
+ fi
+ # Skip over the encoding of the length of the OCTET STRING.
+ if [[ "${dercert:0:1}" == "8" ]]; then
+ i="${dercert:1:1}"
+ i=2*$i+2
+ dercert="${dercert:i}"
+ else
+ dercert="${dercert:2}"
+ fi
+ # Next byte should be a 30 (SEQUENCE).
+ if [[ "${dercert:0:2}" == "30" ]]; then
+ # Get the length of the subjectAltName extension and then skip
+ # over the encoding of the length.
+ if [[ "${dercert:2:1}" == "8" ]]; then
+ case "${dercert:3:1}" in
+ 1) len=2*0x${dercert:4:2}; dercert="${dercert:6}" ;;
+ 2) len=2*0x${dercert:4:4}; dercert="${dercert:8}" ;;
+ 3) len=2*0x${dercert:4:6}; dercert="${dercert:10}" ;;
+ *) len=0 ;;
+ esac
+ else
+ len=2*0x${dercert:2:2}
+ dercert="${dercert:4}"
+ fi
+ if [[ $len -ne 0 ]] && [[ $len -lt ${#dercert} ]]; then
+ # loop through all the names and extract the SRV-ID and XmppAddr identifiers
+ for (( i=0; i < len; i=i+len_name )); do
+ tag="${dercert:i:2}"
+ i+=2
+ if [[ "${dercert:i:1}" == "8" ]]; then
+ i+=1
+ case "${dercert:i:1}" in
+ 1) i+=1; len_name=2*0x${dercert:i:2}; i+=2 ;;
+ 2) i+=1; len_name=2*0x${dercert:i:4}; i+=4 ;;
+ 3) i+=1; len_name=2*0x${dercert:i:6}; i+=4 ;;
+ *) len=0 ;;
+ esac
+ else
+ len_name=2*0x${dercert:i:2}
+ i+=2
+ fi
+ if [[ "$tag" == "A0" ]]; then
+ # This is an otherName.
+ if [[ $len_name -gt 18 ]] && ( [[ "${dercert:i:20}" == "06082B06010505070805" ]] || \
+ [[ "${dercert:i:20}" == "06082B06010505070807" ]] ); then
+ # According to the OID, this is either an SRV-ID or XmppAddr.
+ j=$i+20
+ if [[ "${dercert:j:2}" == "A0" ]]; then
+ j+=2
+ if [[ "${dercert:j:1}" == "8" ]]; then
+ j+=1
+ j+=2*0x${dercert:j:1}+1
+ else
+ j+=2
+ fi
+ if ( [[ "${dercert:i:20}" == "06082B06010505070805" ]] && [[ "${dercert:j:2}" == "0C" ]] ) || \
+ ( [[ "${dercert:i:20}" == "06082B06010505070807" ]] && [[ "${dercert:j:2}" == "16" ]] ); then
+ # XmppAddr should be encoded as UTF8STRING (0C) and
+ # SRV-ID should be encoded IA5STRING (16).
+ j+=2
+ if [[ "${dercert:j:1}" == "8" ]]; then
+ j+=1
+ case "${dercert:j:1}" in
+ 1) j+=1; len1=2*0x${dercert:j:2}; j+=2 ;;
+ 2) j+=1; len1=2*0x${dercert:j:4}; j+=4 ;;
+ 3) j+=1; len1=2*0x${dercert:j:6}; j+=6 ;;
+ 4) len1=0 ;;
+ esac
+ else
+ len1=2*0x${dercert:j:2}
+ j+=2
+ fi
+ if [[ $len1 -ne 0 ]]; then
+ san="$(asciihex_to_binary "${dercert:j:len1}")"
+ if [[ "${dercert:i:20}" == "06082B06010505070805" ]]; then
+ xmppaddr+="$san "
+ else
+ srv_id+="$san "
+ fi
+ fi
+ fi
+ fi
+ fi
+ fi
+ done
+ fi
+ fi
+ [[ -n "$srv_id" ]] && HAS_DNS_SANS=true
+ [[ -n "$xmppaddr" ]] && HAS_DNS_SANS=true
+ while read -d " " san; do
+ [[ -n "$san" ]] && [[ $(toupper "$san") == "_XMPP-SERVER.$servername" ]] && subret=1 && break
+ done <<< "$srv_id"
+ if [[ $subret -eq 0 ]]; then
+ while read -d " " san; do
+ [[ -n "$san" ]] && [[ $(toupper "$san") == "$servername" ]] && subret=1 && break
+ done <<< "$xmppaddr"
+ fi
+ fi
+
+ # Check whether any of the DNS names in the certificate are wildcard names
+ # that match the servername
+ if [[ $subret -eq 0 ]]; then
+ while read san; do
+ [[ -n "$san" ]] || continue
+ wildcard_match "$servername" "$san"
+ [[ $? -eq 0 ]] && subret=2 && break
+ done <<< "$dns_sans"
+ fi
+
+ cn="$(get_cn_from_cert "$cert")"
+
+ # If the CN contains any characters that are not valid for a DNS name,
+ # then assume it does not contain a DNS name.
+ [[ -n $(sed 's/^[_\.a-zA-Z0-9*\-]*//' <<< "$cn") ]] && return $subret
+
+ # Check whether the CN in the certificate matches the servername
+ [[ $(toupper "$cn") == "$servername" ]] && subret+=4 && return $subret
+
+ # Check whether the CN in the certificate is a wildcard name that matches
+ # the servername
+ wildcard_match "$servername" "$cn"
+ [[ $? -eq 0 ]] && subret+=8
+ return $subret
+}
+
+# This function determines whether the certificate (arg3) contains "visibility
+# information" (see Section 4.3.3 of
+# https://www.etsi.org/deliver/etsi_ts/103500_103599/10352303/01.01.01_60/ts_10352303v010101p.pdf .
+etsi_etls_visibility_info() {
+ local jsonID="$1"
+ local spaces="$2"
+ local cert="$3"
+ local cert_txt="$4"
+ local dercert tag
+ local -a fingerprint=() access_description=()
+ local -i i j len len1 len_name nr_visnames=0
+
+ # If "visibility information" is present, it will appear in the subjectAltName
+ # extension (0603551D11) as an otherName with OID 0.4.0.3523.3.1 (060604009B430301).
+ # OpenSSL displays all names of type otherName as "othername:<unsupported>".
+ # As certificates will rarely include a name encoded as an otherName, check the
+ # text version of the certificate for "othername:<unsupported>" before calling
+ # external functions to obtain the DER encoded certificate.
+ if [[ "$cert_txt" =~ X509v3\ Subject\ Alternative\ Name:.*othername:\<unsupported\> ]]; then
+ dercert="$($OPENSSL x509 -in "$cert" -outform DER 2>>$ERRFILE | hexdump -v -e '16/1 "%02X"')"
+ if [[ "$dercert" =~ 0603551D110101FF04[0-9A-F]*060604009B430301 ]] || \
+ [[ "$dercert" =~ 0603551D1104[0-9A-F]*060604009B430301 ]]; then
+ # Look for the beginning of the subjectAltName extension. It
+ # will begin with the OID (2.5.29.17 = 0603551D11). After the OID
+ # there may be an indication that the extension is critical (0101FF).
+ # Finally will be the tag indicating that the value of the extension is
+ # encoded as an OCTET STRING (04).
+ if [[ "$dercert" =~ 0603551D110101FF04 ]]; then
+ dercert="${dercert##*0603551D110101FF04}"
+ else
+ dercert="${dercert##*0603551D1104}"
+ fi
+ # Skip over the encoding of the length of the OCTET STRING.
+ if [[ "${dercert:0:1}" == 8 ]]; then
+ i="${dercert:1:1}"
+ i=2*$i+2
+ dercert="${dercert:i}"
+ else
+ dercert="${dercert:2}"
+ fi
+ # Next byte should be a 30 (SEQUENCE).
+ if [[ "${dercert:0:2}" == 30 ]]; then
+ # Get the length of the subjectAltName extension and then skip
+ # over the encoding of the length.
+ if [[ "${dercert:2:1}" == 8 ]]; then
+ case "${dercert:3:1}" in
+ 1) len=2*0x${dercert:4:2}; dercert="${dercert:6}" ;;
+ 2) len=2*0x${dercert:4:4}; dercert="${dercert:8}" ;;
+ 3) len=2*0x${dercert:4:6}; dercert="${dercert:10}" ;;
+ *) len=0 ;;
+ esac
+ else
+ len=2*0x${dercert:2:2}
+ dercert="${dercert:4}"
+ fi
+ if [[ $len -ne 0 ]] && [[ $len -lt ${#dercert} ]]; then
+ # loop through all the names and extract the visibility information
+ for (( i=0; i < len; i=i+len_name )); do
+ tag="${dercert:i:2}"
+ i+=2
+ if [[ "${dercert:i:1}" == 8 ]]; then
+ i+=1
+ case "${dercert:i:1}" in
+ 1) i+=1; len_name=2*0x${dercert:i:2}; i+=2 ;;
+ 2) i+=1; len_name=2*0x${dercert:i:4}; i+=4 ;;
+ 3) i+=1; len_name=2*0x${dercert:i:6}; i+=4 ;;
+ *) len=0 ;;
+ esac
+ else
+ len_name=2*0x${dercert:i:2}
+ i+=2
+ fi
+ [[ "$tag" == A0 ]] || continue
+ # This is an otherName.
+ [[ $len_name -gt 16 ]] || continue
+ [[ "${dercert:i:16}" == 060604009B430301 ]] || continue
+ # According to the OID, this is visibility information.
+ j=$i+16
+ # Skip over the tag (A0) and length for the otherName value.
+ [[ "${dercert:j:2}" == A0 ]] || continue
+ j+=2
+ if [[ "${dercert:j:1}" == 8 ]]; then
+ j+=1
+ j+=2*0x${dercert:j:1}+1
+ else
+ j+=2
+ fi
+ # The value for this otherName is encoded as a SEQUENCE (30):
+ # VisibilityInformation ::= SEQUENCE {
+ # fingerprint OCTET STRING (SIZE(10)),
+ # accessDescription UTF8String }
+ [[ "${dercert:j:2}" == 30 ]] || continue
+ j+=2
+ if [[ "${dercert:j:1}" == 8 ]]; then
+ j+=1
+ case "${dercert:j:1}" in
+ 1) j+=1; len1=2*0x${dercert:j:2}; j+=2 ;;
+ 2) j+=1; len1=2*0x${dercert:j:4}; j+=4 ;;
+ 3) j+=1; len1=2*0x${dercert:j:6}; j+=6 ;;
+ 4) len1=0 ;;
+ esac
+ else
+ len1=2*0x${dercert:j:2}
+ j+=2
+ fi
+ [[ $len1 -ne 0 ]] || continue
+ # Next is the 10-byte fingerprint, encoded as an OCTET STRING (04)
+ [[ "${dercert:j:4}" == 040A ]] || continue
+ j+=4
+ fingerprint[nr_visnames]="$(asciihex_to_binary "${dercert:j:20}")"
+ j+=20
+ # Finally comes the access description, encoded as a UTF8String (0C).
+ [[ "${dercert:j:2}" == 0C ]] || continue
+ j+=2
+ if [[ "${dercert:j:1}" == "8" ]]; then
+ j+=1
+ case "${dercert:j:1}" in
+ 1) j+=1; len1=2*0x${dercert:j:2}; j+=2 ;;
+ 2) j+=1; len1=2*0x${dercert:j:4}; j+=4 ;;
+ 3) j+=1; len1=2*0x${dercert:j:6}; j+=6 ;;
+ 4) len1=0 ;;
+ esac
+ else
+ len1=2*0x${dercert:j:2}
+ j+=2
+ fi
+ access_description[nr_visnames]=""$(asciihex_to_binary "${dercert:j:len1}")""
+ nr_visnames+=1
+ done
+ fi
+ fi
+ fi
+ fi
+ if [[ $nr_visnames -eq 0 ]]; then
+ outln "not present"
+ fileout "$jsonID" "INFO" "not present"
+ else
+ for (( i=0; i < nr_visnames; i++ )); do
+ [[ $i -ne 0 ]] && out "$spaces"
+ outln "$(out_row_aligned_max_width "${fingerprint[i]} / ${access_description[i]}" "$spaces" $TERM_WIDTH)"
+ fileout "$jsonID" "INFO" "${fingerprint[i]} / ${access_description[i]}"
+ done
+ fi
+ return 0
+}
+
+# NOTE: arg3 must contain the text output of $HOSTCERT.
+must_staple() {
+ local jsonID="cert_mustStapleExtension"
+ local json_postfix="$1"
+ local provides_stapling="$2"
+ local hostcert_txt="$3"
+ local cert extn
+ local -i extn_len
+ local supported=false
+
+ # Note this function is only looking for status_request (5) and not
+ # status_request_v2 (17), since OpenSSL seems to only include status_request (5)
+ # in its ClientHello when the "-status" option is used.
+
+ # OpenSSL 1.1.0 supports pretty-printing the "TLS Feature extension." For any
+ # previous versions of OpenSSL, OpenSSL can only show if the extension OID is present.
+ if grep -A 1 "TLS Feature:" <<< "$hostcert_txt" | grep -q "status_request"; then
+ # FIXME: This will indicate that must staple is supported if the
+ # certificate indicates status_request or status_request_v2. This is
+ # probably okay, since it seems likely that any TLS Feature extension
+ # that includes status_request_v2 will also include status_request.
+ supported=true
+ elif [[ "$hostcert_txt" =~ '1.3.6.1.5.5.7.1.24:' ]]; then
+ cert="$($OPENSSL x509 -in "$HOSTCERT" -outform DER 2>>$ERRFILE | hexdump -v -e '16/1 "%02X"')"
+ extn="${cert##*06082B06010505070118}"
+ # Check for critical bit, and skip over it if present.
+ [[ "${extn:0:6}" == "0101FF" ]] && extn="${extn:6}"
+ # Next is tag and length of extnValue OCTET STRING. Assume it is less than 128 bytes.
+ extn="${extn:4}"
+ # The TLS Feature is a SEQUENCE of INTEGER. Get the length of the SEQUENCE
+ extn_len=2*$(hex2dec "${extn:2:2}")
+ # If the extension include the status_request (5), then it supports must staple.
+ if [[ "${extn:4:extn_len}" =~ 020105 ]]; then
+ supported=true
+ fi
+ fi
+
+ if "$supported"; then
+ if "$provides_stapling"; then
+ prln_svrty_good "supported"
+ fileout "${jsonID}${json_postfix}" "OK" "supported"
+ else
+ prln_svrty_high "requires OCSP stapling (NOT ok)"
+ fileout "${jsonID}${json_postfix}" "HIGH" "extension detected but no OCSP stapling provided"
+ fi
+ else
+ outln "--"
+ fileout "${jsonID}${json_postfix}" "INFO" "--"
+ fi
+ return 0
+}
+
+# TODO: This function checks for Certificate Transparency support based on RFC 6962.
+# It will need to be updated to add checks for Certificate Transparency support based on 6962bis.
+# return values are results, no error conditions
+certificate_transparency() {
+ local cert_txt="$1"
+ local ocsp_response="$2"
+ local -i number_of_certificates=$3
+ local cipher="$4"
+ local sni_used="$5"
+ local tls_version="$6"
+ local sni=""
+ local ciphers=""
+ local hexc n ciph sslver kx auth enc mac export
+ local extra_extns=""
+ local -i success
+ # Cipher suites that use a certificate with an RSA (signature) public key
+ local -r a_rsa="cc,13, cc,15, c0,30, c0,28, c0,14, 00,9f, cc,a8, cc,aa, c0,a3, c0,9f, 00,6b, 00,39, c0,77, 00,c4, 00,88, c0,45, c0,4d, c0,53, c0,61, c0,7d, c0,8b, 16,b7, 16,b9, c0,2f, c0,27, c0,13, 00,9e, c0,a2, c0,9e, 00,67, 00,33, c0,76, 00,be, 00,9a, 00,45, c0,44, c0,4c, c0,52, c0,60, c0,7c, c0,8a, c0,11, c0,12, 00,16, 00,15, 00,14, c0,10"
+ # Cipher suites that use a certificate with an RSA (encryption) public key
+ local -r e_rsa="00,b7, c0,99, 00,ad, cc,ae, 00,9d, c0,a1, c0,9d, 00,3d, 00,35, 00,c0, 00,84, 00,95, c0,3d, c0,51, c0,69, c0,6f, c0,7b, c0,93, ff,01, 00,ac, c0,a0, c0,9c, 00,9c, 00,3c, 00,2f, 00,ba, 00,b6, 00,96, 00,41, c0,98, 00,07, 00,94, c0,3c, c0,50, c0,68, c0,6e, c0,7a, c0,92, 00,05, 00,04, 00,92, 00,0a, 00,93, fe,ff, ff,e0, 00,62, 00,09, 00,61, fe,fe, ff,e1, 00,64, 00,60, 00,08, 00,06, 00,03, 00,b9, 00,b8, 00,2e, 00,3b, 00,02, 00,01, ff,00"
+ # Cipher suites that use a certificate with a DSA public key
+ local -r a_dss="00,a3, 00,6a, 00,38, 00,c3, 00,87, c0,43, c0,57, c0,81, 00,a2, 00,40, 00,32, 00,bd, 00,99, 00,44, c0,42, c0,56, c0,80, 00,66, 00,13, 00,63, 00,12, 00,65, 00,11"
+ # Cipher suites that use a certificate with a DH public key
+ local -r a_dh="00,a5, 00,a1, 00,69, 00,68, 00,37, 00,36, 00,c2, 00,c1, 00,86, 00,85, c0,3f, c0,41, c0,55, c0,59, c0,7f, c0,83, 00,a4, 00,a0, 00,3f, 00,3e, 00,31, 00,30, 00,bc, 00,bb, 00,98, 00,97, 00,43, 00,42, c0,3e, c0,40, c0,54, c0,58, c0,7e, c0,82, 00,10, 00,0d, 00,0f, 00,0c, 00,0b, 00,0e"
+ # Cipher suites that use a certificate with an ECDH public key
+ local -r a_ecdh="c0,32, c0,2e, c0,2a, c0,26, c0,0f, c0,05, c0,79, c0,75, c0,4b, c0,4f, c0,5f, c0,63, c0,89, c0,8d, c0,31, c0,2d, c0,29, c0,25, c0,0e, c0,04, c0,78, c0,74, c0,4a, c0,4e, c0,5e, c0,62, c0,88, c0,8c, c0,0c, c0,02, c0,0d, c0,03, c0,0b, c0,01"
+ # Cipher suites that use a certificate with an ECDSA public key
+ local -r a_ecdsa="cc,14, c0,2c, c0,24, c0,0a, cc,a9, c0,af, c0,ad, c0,73, c0,49, c0,5d, c0,87, 16,b8, 16,ba, c0,2b, c0,23, c0,09, c0,ae, c0,ac, c0,72, c0,48, c0,5c, c0,86, c0,07, c0,08, c0,06"
+ # Cipher suites that use a certificate with a GOST public key
+ local -r a_gost="00,80, 00,81, 00,82, 00,83"
+
+ # First check whether signed certificate timestamps (SCT) are included in the
+ # server's certificate. If they aren't, check whether the server provided
+ # a stapled OCSP response with SCTs. If no SCTs were found in the certificate
+ # or OCSP response, check for an SCT TLS extension.
+ if [[ "$cert_txt" =~ CT\ Precertificate\ SCTs ]] || [[ "$cert_txt" =~ '1.3.6.1.4.1.11129.2.4.2' ]]; then
+ tm_out "certificate extension"
+ return 0
+ fi
+ if [[ "$ocsp_response" =~ CT\ Certificate\ SCTs ]] || [[ "$ocsp_response" =~ '1.3.6.1.4.1.11129.2.4.5' ]]; then
+ tm_out "OCSP extension"
+ return 0
+ fi
+
+ # If the server only has one certificate, then it is sufficient to check whether
+ # determine_tls_extensions() discovered an SCT TLS extension. If the server has more than
+ # one certificate, then it is possible that an SCT TLS extension is returned for some
+ # certificates, but not for all of them.
+ if [[ $number_of_certificates -eq 1 ]] && [[ "$TLS_EXTENSIONS" =~ signed\ certificate\ timestamps ]]; then
+ tm_out "TLS extension"
+ return 0
+ fi
+
+ if [[ $number_of_certificates -gt 1 ]] && ! "$SSL_NATIVE"; then
+ if [[ "$tls_version" == 0304 ]]; then
+ ciphers=", 13,01, 13,02, 13,03, 13,04, 13,05"
+ if [[ "$cipher" == tls1_3_RSA ]]; then
+ extra_extns=", 00,0d,00,10,00,0e,08,04,08,05,08,06,04,01,05,01,06,01,02,01"
+ elif [[ "$cipher" == tls1_3_ECDSA ]]; then
+ extra_extns=", 00,0d,00,0a,00,08,04,03,05,03,06,03,02,03"
+ else
+ return 1
+ fi
+ else
+ [[ "$cipher" =~ aRSA ]] && ciphers+=", $a_rsa"
+ [[ "$cipher" =~ eRSA ]] && ciphers+=", $e_rsa"
+ [[ "$cipher" =~ aDSS ]] && ciphers+=", $a_dss"
+ [[ "$cipher" =~ aDH ]] && ciphers+=", $a_dh"
+ [[ "$cipher" =~ aECDH ]] && ciphers+=", $a_ecdh"
+ [[ "$cipher" =~ aECDSA ]] && ciphers+=", $a_ecdsa"
+ [[ "$cipher" =~ aGOST ]] && ciphers+=", $a_gost"
+
+ [[ -z "$ciphers" ]] && return 1
+ ciphers+=", 00,ff"
+ fi
+ [[ -z "$sni_used" ]] && sni="$SNI" && SNI=""
+ tls_sockets "${tls_version:2:2}" "${ciphers:2}" "all" "00,12,00,00$extra_extns"
+ success=$?
+ [[ -z "$sni_used" ]] && SNI="$sni"
+ if ( [[ $success -eq 0 ]] || [[ $success -eq 2 ]] ) && \
+ grep -a 'TLS server extension ' "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt" | \
+ grep -aq "signed certificate timestamps"; then
+ tm_out "TLS extension"
+ return 0
+ fi
+ fi
+
+ if [[ $SERVICE != HTTP ]] && ! "$CLIENT_AUTH"; then
+ # At the moment Certificate Transparency only applies to HTTPS.
+ tm_out "N/A"
+ else
+ tm_out "--"
+ fi
+ return 0
+}
+
+certificate_info() {
+ local proto
+ local -i certificate_number=$1
+ local -i number_of_certificates=$2
+ local cert_txt="$3"
+ local cipher=$4
+ local cert_keysize=$5
+ local cert_type="$6"
+ local ocsp_response_binary="$7"
+ local ocsp_response=$8
+ local ocsp_response_status=$9
+ local sni_used="${10}"
+ local ct="${11}"
+ local certificate_list_ordering_problem="${12}"
+ local cert_sig_algo cert_sig_hash_algo cert_key_algo cert_keyusage cert_ext_keyusage short_keyAlgo
+ local outok=true
+ local expire days2expire secs2warn ocsp_uri crl
+ local startdate enddate issuer_CN issuer_C issuer_O issuer sans san all_san="" cn
+ local issuer_DC issuerfinding cn_nosni=""
+ local cert_fingerprint_sha1 cert_fingerprint_sha2 cert_serial
+ local policy_oid
+ local spaces=""
+ local -i trust_sni=0 trust_nosni=0 diffseconds=0
+ local has_dns_sans has_dns_sans_nosni
+ local trust_sni_finding
+ local -i certificates_provided
+ local cnfinding trustfinding trustfinding_nosni
+ local cnok="OK"
+ local expfinding expok="OK"
+ local -i ret=0
+ local json_postfix="" # string to place at the end of JSON IDs when there is more than one certificate
+ local jsonID="" # string to place at beginning of JSON IDs
+ local indent=""
+ local days2warn2=$DAYS2WARN2
+ local days2warn1=$DAYS2WARN1
+ local provides_stapling=false
+ local caa_node="" all_caa="" caa_property_name="" caa_property_value=""
+ local response=""
+ local yearstart yearend clockstart clockend y m d
+ local gt_825=false gt_825warn=false
+
+ if [[ $number_of_certificates -gt 1 ]]; then
+ [[ $certificate_number -eq 1 ]] && outln
+ indent=" "
+ out "$indent"
+ pr_headline "Server Certificate #$certificate_number"
+ [[ -z "$sni_used" ]] && pr_underline " (in response to request w/o SNI)"
+ outln
+ json_postfix=" <cert#${certificate_number}>"
+ spaces=" "
+ else
+ spaces=" "
+ fi
+
+ GOOD_CA_BUNDLE=""
+ cert_sig_algo="$(awk -F':' '/Signature Algorithm/ { print $2; if (++Match >= 1) exit; }' <<< "$cert_txt")"
+ cert_sig_algo="${cert_sig_algo// /}"
+ cert_key_algo="$(awk -F':' '/Public Key Algorithm:/ { print $2; if (++Match >= 1) exit; }' <<< "$cert_txt")"
+ cert_key_algo="${cert_key_algo// /}"
+
+ out "$indent" ; pr_bold " Signature Algorithm "
+ jsonID="cert_signatureAlgorithm"
+ case $cert_sig_algo in
+ sha1WithRSAEncryption)
+ pr_svrty_medium "SHA1 with RSA"
+ if [[ "$SERVICE" == HTTP ]] || "$ASSUME_HTTP"; then
+ out " -- besides: users will receive a "; pr_svrty_high "strong browser WARNING"
+ fi
+ outln
+ fileout "${jsonID}${json_postfix}" "MEDIUM" "SHA1 with RSA"
+ ;;
+ sha224WithRSAEncryption)
+ outln "SHA224 with RSA"
+ fileout "${jsonID}${json_postfix}" "INFO" "SHA224 with RSA"
+ ;;
+ sha256WithRSAEncryption)
+ prln_svrty_good "SHA256 with RSA"
+ fileout "${jsonID}${json_postfix}" "OK" "SHA256 with RSA"
+ ;;
+ sha384WithRSAEncryption)
+ prln_svrty_good "SHA384 with RSA"
+ fileout "${jsonID}${json_postfix}" "OK" "SHA384 with RSA"
+ ;;
+ sha512WithRSAEncryption)
+ prln_svrty_good "SHA512 with RSA"
+ fileout "${jsonID}${json_postfix}" "OK" "SHA512 with RSA"
+ ;;
+ ecdsa-with-SHA1)
+ prln_svrty_medium "ECDSA with SHA1"
+ fileout "${jsonID}${json_postfix}" "MEDIUM" "ECDSA with SHA1"
+ ;;
+ ecdsa-with-SHA224)
+ outln "ECDSA with SHA224"
+ fileout "${jsonID}${json_postfix}" "INFO" "ECDSA with SHA224"
+ ;;
+ ecdsa-with-SHA256)
+ prln_svrty_good "ECDSA with SHA256"
+ fileout "${jsonID}${json_postfix}" "OK" "ECDSA with SHA256"
+ ;;
+ ecdsa-with-SHA384)
+ prln_svrty_good "ECDSA with SHA384"
+ fileout "${jsonID}${json_postfix}" "OK" "ECDSA with SHA384"
+ ;;
+ ecdsa-with-SHA512)
+ prln_svrty_good "ECDSA with SHA512"
+ fileout "${jsonID}${json_postfix}" "OK" "ECDSA with SHA512"
+ ;;
+ dsaWithSHA1)
+ prln_svrty_medium "DSA with SHA1"
+ fileout "${jsonID}${json_postfix}" "MEDIUM" "DSA with SHA1"
+ ;;
+ dsa_with_SHA224)
+ outln "DSA with SHA224"
+ fileout "${jsonID}${json_postfix}" "INFO" "DSA with SHA224"
+ ;;
+ dsa_with_SHA256)
+ prln_svrty_good "DSA with SHA256"
+ fileout "${jsonID}${json_postfix}" "OK" "DSA with SHA256"
+ ;;
+ rsassaPss)
+ cert_sig_hash_algo="$(grep -A 1 "Signature Algorithm" <<< "$cert_txt" | head -2 | tail -1 | sed 's/^.*Hash Algorithm: //')"
+ case $cert_sig_hash_algo in
+ sha1)
+ prln_svrty_medium "RSASSA-PSS with SHA1"
+ fileout "${jsonID}${json_postfix}" "MEDIUM" "RSASSA-PSS with SHA1"
+ ;;
+ sha224)
+ outln "RSASSA-PSS with SHA224"
+ fileout "${jsonID}${json_postfix}" "INFO" "RSASSA-PSS with SHA224"
+ ;;
+ sha256)
+ prln_svrty_good "RSASSA-PSS with SHA256"
+ fileout "${jsonID}${json_postfix}" "OK" "RSASSA-PSS with SHA256"
+ ;;
+ sha384)
+ prln_svrty_good "RSASSA-PSS with SHA384"
+ fileout "${jsonID}${json_postfix}" "OK" "RSASSA-PSS with SHA384"
+ ;;
+ sha512)
+ prln_svrty_good "RSASSA-PSS with SHA512"
+ fileout "${jsonID}${json_postfix}" "OK" "RSASSA-PSS with SHA512"
+ ;;
+ *)
+ out "RSASSA-PSS with $cert_sig_hash_algo"
+ prln_warning " (Unknown hash algorithm)"
+ fileout "${jsonID}${json_postfix}" "DEBUG" "RSASSA-PSS with $cert_sig_hash_algo"
+ esac
+ ;;
+ md2*)
+ prln_svrty_critical "MD2"
+ fileout "${jsonID}${json_postfix}" "CRITICAL" "MD2"
+ ;;
+ md4*)
+ prln_svrty_critical "MD4"
+ fileout "${jsonID}${json_postfix}" "CRITICAL" "MD4"
+ ;;
+ md5*)
+ prln_svrty_critical "MD5"
+ fileout "${jsonID}${json_postfix}" "CRITICAL" "MD5"
+ ;;
+ *)
+ out "$cert_sig_algo ("
+ pr_warning "FIXME: can't tell whether this is good or not"
+ outln ")"
+ fileout "${jsonID}${json_postfix}" "DEBUG" "$cert_sig_algo"
+ ((ret++))
+ ;;
+ esac
+ # old, but still interesting: https://blog.hboeck.de/archives/754-Playing-with-the-EFF-SSL-Observatory.html
+
+ out "$indent"; pr_bold " Server key size "
+ jsonID="cert_keySize"
+ if [[ -z "$cert_keysize" ]]; then
+ outln "(couldn't determine)"
+ fileout "${jsonID}${json_postfix}" "cannot be determined"
+ ((ret++))
+ else
+ case $cert_key_algo in
+ *RSA*|*rsa*) short_keyAlgo="RSA";;
+ *ecdsa*|*ecPublicKey) short_keyAlgo="EC";;
+ *DSA*|*dsa*) short_keyAlgo="DSA";;
+ *GOST*|*gost*) short_keyAlgo="GOST";;
+ *dh*|*DH*) short_keyAlgo="DH" ;;
+ *) pr_fixme "don't know $cert_key_algo "
+ let ret++ ;;
+ esac
+ out "$short_keyAlgo "
+ # https://tools.ietf.org/html/rfc4492, https://www.keylength.com/en/compare/
+ # https://infoscience.epfl.ch/record/164526/files/NPDF-22.pdf
+ # see https://csrc.nist.gov/publications/detail/sp/800-57-part-1/rev-4/final
+ # Table 2 @ chapter 5.6.1 (~ p66)
+ if [[ $cert_key_algo =~ ecdsa ]] || [[ $cert_key_algo =~ ecPublicKey ]]; then
+ if [[ "$cert_keysize" -le 110 ]]; then # a guess
+ pr_svrty_critical "$cert_keysize"
+ fileout "${jsonID}${json_postfix}" "CRITICAL" "$short_keyAlgo $cert_keysize bits"
+ elif [[ "$cert_keysize" -le 123 ]]; then # a guess
+ pr_svrty_high "$cert_keysize"
+ fileout "${jsonID}${json_postfix}" "HIGH" "$short_keyAlgo $cert_keysize bits"
+ elif [[ "$cert_keysize" -le 163 ]]; then
+ pr_svrty_medium "$cert_keysize"
+ fileout "${jsonID}${json_postfix}" "MEDIUM" "$short_keyAlgo $cert_keysize bits"
+ elif [[ "$cert_keysize" -le 224 ]]; then
+ out "$cert_keysize"
+ fileout "${jsonID}${json_postfix}" "INFO" "$short_keyAlgo $cert_keysize bits"
+ elif [[ "$cert_keysize" -le 533 ]]; then
+ pr_svrty_good "$cert_keysize"
+ fileout "${jsonID}${json_postfix}" "OK" "$short_keyAlgo $cert_keysize bits"
+ else
+ out "keysize: $cert_keysize (not expected, FIXME)"
+ fileout "${jsonID}${json_postfix}" "DEBUG" " $cert_keysize bits (not expected)"
+ ((ret++))
+ fi
+ outln " bits"
+ elif [[ $cert_key_algo =~ RSA ]] || [[ $cert_key_algo =~ rsa ]] || [[ $cert_key_algo =~ dsa ]] || \
+ [[ $cert_key_algo =~ dhKeyAgreement ]] || [[ $cert_key_algo == X9.42\ DH ]]; then
+ if [[ "$cert_keysize" -le 512 ]]; then
+ pr_svrty_critical "$cert_keysize"
+ outln " bits"
+ fileout "${jsonID}${json_postfix}" "CRITICAL" "$short_keyAlgo $cert_keysize bits"
+ elif [[ "$cert_keysize" -le 768 ]]; then
+ pr_svrty_high "$cert_keysize"
+ outln " bits"
+ fileout "${jsonID}${json_postfix}" "HIGH" "$short_keyAlgo $cert_keysize bits"
+ elif [[ "$cert_keysize" -le 1024 ]]; then
+ pr_svrty_medium "$cert_keysize"
+ outln " bits"
+ fileout "${jsonID}${json_postfix}" "MEDIUM" "$short_keyAlgo $cert_keysize bits"
+ elif [[ "$cert_keysize" -le 2048 ]]; then
+ outln "$cert_keysize bits"
+ fileout "${jsonID}${json_postfix}" "INFO" "$short_keyAlgo $cert_keysize bits"
+ elif [[ "$cert_keysize" -le 4096 ]]; then
+ pr_svrty_good "$cert_keysize"
+ fileout "${jsonID}${json_postfix}" "OK" "$short_keyAlgo $cert_keysize bits"
+ outln " bits"
+ else
+ pr_warning "weird key size: $cert_keysize bits"; outln " (could cause compatibility problems)"
+ fileout "${jsonID}${json_postfix}" "WARN" "$cert_keysize bits (Odd)"
+ ((ret++))
+ fi
+ else
+ out "$cert_key_algo + $cert_keysize bits ("
+ pr_warning "FIXME: can't tell whether this is good or not"
+ outln ")"
+ fileout "${jsonID}${json_postfix}" "WARN" "Server keys $cert_keysize bits, unknown public key algorithm $cert_key_algo"
+ ((ret++))
+ fi
+ fi
+
+ out "$indent"; pr_bold " Server key usage ";
+ outok=true
+ jsonID="cert_keyUsage"
+ cert_keyusage="$(strip_leading_space "$(awk '/X509v3 Key Usage:/ { getline; print $0 }' <<< "$cert_txt")")"
+ if [[ -n "$cert_keyusage" ]]; then
+ outln "$cert_keyusage"
+ if ( [[ " $cert_type " =~ " RSASig " ]] || [[ " $cert_type " =~ " DSA " ]] || [[ " $cert_type " =~ " ECDSA " ]] ) && \
+ [[ ! "$cert_keyusage" =~ "Digital Signature" ]]; then
+ prln_svrty_high "$indent Certificate incorrectly used for digital signatures"
+ fileout "${jsonID}${json_postfix}" "HIGH" "Certificate incorrectly used for digital signatures: \"$cert_keyusage\""
+ outok=false
+ fi
+ if [[ " $cert_type " =~ " RSAKMK " ]] && [[ ! "$cert_keyusage" =~ "Key Encipherment" ]]; then
+ prln_svrty_high "$indent Certificate incorrectly used for key encipherment"
+ fileout "${jsonID}${json_postfix}" "HIGH" "Certificate incorrectly used for key encipherment: \"$cert_keyusage\""
+ outok=false
+ fi
+ if ( [[ " $cert_type " =~ " DH " ]] || [[ " $cert_type " =~ " ECDH " ]] ) && \
+ [[ ! "$cert_keyusage" =~ "Key Agreement" ]]; then
+ prln_svrty_high "$indent Certificate incorrectly used for key agreement"
+ fileout "${jsonID}${json_postfix}" "HIGH" "Certificate incorrectly used for key agreement: \"$cert_keyusage\""
+ outok=false
+ fi
+ else
+ outln "--"
+ fileout "${jsonID}${json_postfix}" "INFO" "No server key usage information"
+ outok=false
+ fi
+ if "$outok"; then
+ fileout "${jsonID}${json_postfix}" "INFO" "$cert_keyusage"
+ fi
+
+ out "$indent"; pr_bold " Server extended key usage ";
+ jsonID="cert_extKeyUsage"
+ outok=true
+ cert_ext_keyusage="$(strip_leading_space "$(awk '/X509v3 Extended Key Usage:/ { getline; print $0 }' <<< "$cert_txt")")"
+ if [[ -n "$cert_ext_keyusage" ]]; then
+ outln "$cert_ext_keyusage"
+ if [[ ! "$cert_ext_keyusage" =~ "TLS Web Server Authentication" ]] && [[ ! "$cert_ext_keyusage" =~ "Any Extended Key Usage" ]]; then
+ prln_svrty_high "$indent Certificate incorrectly used for TLS Web Server Authentication"
+ fileout "${jsonID}${json_postfix}" "HIGH" "Certificate incorrectly used for TLS Web Server Authentication: \"$cert_ext_keyusage\""
+ outok=false
+ fi
+ else
+ outln "--"
+ fileout "${jsonID}${json_postfix}" "INFO" "No server extended key usage information"
+ outok=false
+ fi
+ if "$outok"; then
+ fileout "${jsonID}${json_postfix}" "INFO" "$cert_ext_keyusage"
+ fi
+
+ out "$indent"; pr_bold " Serial / Fingerprints "
+ cert_serial="$($OPENSSL x509 -noout -in $HOSTCERT -serial 2>>$ERRFILE | sed 's/serial=//')"
+ fileout "cert_serialNumber${json_postfix}" "INFO" "$cert_serial"
+
+ cert_fingerprint_sha1="$($OPENSSL x509 -noout -in $HOSTCERT -fingerprint -sha1 2>>$ERRFILE | sed 's/Fingerprint=//' | sed 's/://g')"
+ fileout "cert_fingerprintSHA1${json_postfix}" "INFO" "${cert_fingerprint_sha1//SHA1 /}"
+ outln "$cert_serial / $cert_fingerprint_sha1"
+
+ cert_fingerprint_sha2="$($OPENSSL x509 -noout -in $HOSTCERT -fingerprint -sha256 2>>$ERRFILE | sed 's/Fingerprint=//' | sed 's/://g' )"
+ fileout "cert_fingerprintSHA256${json_postfix}" "INFO" "${cert_fingerprint_sha2//SHA256 /}"
+ outln "$spaces$cert_fingerprint_sha2"
+
+ # " " needs to be converted back to lf in JSON/CSV output
+ fileout "cert${json_postfix}" "INFO" "$(< $HOSTCERT)"
+
+ [[ -z $CERT_FINGERPRINT_SHA2 ]] && \
+ CERT_FINGERPRINT_SHA2="$cert_fingerprint_sha2" ||
+ CERT_FINGERPRINT_SHA2="$cert_fingerprint_sha2 $CERT_FINGERPRINT_SHA2"
+ [[ -z $RSA_CERT_FINGERPRINT_SHA2 ]] && \
+ ( [[ $cert_key_algo = *RSA* ]] || [[ $cert_key_algo = *rsa* ]] ) &&
+ RSA_CERT_FINGERPRINT_SHA2="$cert_fingerprint_sha2"
+
+ out "$indent"; pr_bold " Common Name (CN) "
+ cnfinding="Common Name (CN) : "
+ cn="$(get_cn_from_cert $HOSTCERT)"
+ if [[ -n "$cn" ]]; then
+ pr_italic "$cn"
+ cnfinding="$cn"
+ else
+ cn="no CN field in subject"
+ out "($cn)"
+ cnfinding="$cn"
+ cnok="INFO"
+ fi
+ fileout "cert_commonName${json_postfix}" "$cnok" "$cnfinding"
+ cnfinding=""
+
+ if [[ -n "$sni_used" ]]; then
+ if grep -q "\-\-\-\-\-BEGIN" "$HOSTCERT.nosni"; then
+ cn_nosni="$(get_cn_from_cert "$HOSTCERT.nosni")"
+ [[ -z "$cn_nosni" ]] && cn_nosni="no CN field in subject"
+ fi
+ debugme tm_out "\"$NODE\" | \"$cn\" | \"$cn_nosni\""
+ else
+ debugme tm_out "\"$NODE\" | \"$cn\""
+ fi
+
+ if [[ -z "$sni_used" ]] || [[ "$(toupper "$cn_nosni")" == "$(toupper "$cn")" ]]; then
+ outln
+ cnfinding="$cn"
+ elif [[ -z "$cn_nosni" ]]; then
+ out " (request w/o SNI didn't succeed";
+ cnfinding+="request w/o SNI didn't succeed"
+ if [[ $cert_sig_algo =~ ecdsa ]]; then
+ out ", usual for EC certificates"
+ cnfinding+=", usual for EC certificates"
+ fi
+ outln ")"
+ cnfinding+=""
+ elif [[ "$cn_nosni" == *"no CN field"* ]]; then
+ outln ", (request w/o SNI: $cn_nosni)"
+ cnfinding="$cn_nosni"
+ else
+ out " (CN in response to request w/o SNI: "; pr_italic "$cn_nosni"; outln ")"
+ cnfinding="$cn_nosni"
+ fi
+ fileout "cert_commonName_wo_SNI${json_postfix}" "INFO" "$cnfinding"
+
+ sans=$(grep -A2 "Subject Alternative Name" <<< "$cert_txt" | \
+ grep -E "DNS:|IP Address:|email:|URI:|DirName:|Registered ID:" | tr ',' '\n' | \
+ sed -e 's/ *DNS://g' -e 's/ *IP Address://g' -e 's/ *email://g' -e 's/ *URI://g' -e 's/ *DirName://g' \
+ -e 's/ *Registered ID://g' \
+ -e 's/ *othername:<unsupported>//g' -e 's/ *X400Name:<unsupported>//g' -e 's/ *EdiPartyName:<unsupported>//g')
+ # ^^^ CACert
+
+ out "$indent"; pr_bold " subjectAltName (SAN) "
+ jsonID="cert_subjectAltName"
+ if [[ -n "$sans" ]]; then
+ while read san; do
+ [[ -n "$san" ]] && all_san+="$san "
+ done <<< "$sans"
+ prln_italic "$(out_row_aligned_max_width "$all_san" "$indent " $TERM_WIDTH)"
+ fileout "${jsonID}${json_postfix}" "INFO" "$all_san"
+ else
+ if [[ $SERVICE == "HTTP" ]] || "$ASSUME_HTTP"; then
+ pr_svrty_high "missing (NOT ok)"; outln " -- Browsers are complaining"
+ fileout "${jsonID}${json_postfix}" "HIGH" "No SAN, browsers are complaining"
+ else
+ pr_svrty_medium "missing"; outln " -- no SAN is deprecated"
+ fileout "${jsonID}${json_postfix}" "MEDIUM" "Providing no SAN is deprecated"
+ fi
+ fi
+
+ out "$indent"; pr_bold " Issuer "
+ jsonID="cert_caIssuers"
+ #FIXME: oid would be better maybe (see above)
+ issuer="$($OPENSSL x509 -in $HOSTCERT -noout -issuer -nameopt multiline,-align,sname,-esc_msb,utf8,-space_eq 2>>$ERRFILE)"
+ issuer_CN="$(awk -F'=' '/CN=/ { print $2 }' <<< "$issuer")"
+ issuer_O="$(awk -F'=' '/O=/ { print $2 }' <<< "$issuer")"
+ issuer_C="$(awk -F'=' '/ C=/ { print $2 }' <<< "$issuer")"
+ issuer_DC="$(awk -F'=' '/DC=/ { print $2 }' <<< "$issuer")"
+
+ if [[ "$issuer_O" == "issuer=" ]] || [[ "$issuer_O" == "issuer= " ]] || [[ "$issuer_CN" == "$cn" ]]; then
+ prln_svrty_critical "self-signed (NOT ok)"
+ fileout "${jsonID}${json_postfix}" "CRITICAL" "selfsigned"
+ else
+ issuerfinding="$issuer_CN"
+ pr_italic "$issuer_CN"
+ if [[ -z "$issuer_O" ]] && [[ -n "$issuer_DC" ]]; then
+ for san in $issuer_DC; do
+ if [[ -z "$issuer_O" ]]; then
+ issuer_O="${san}"
+ else
+ issuer_O="${san}.${issuer_O}"
+ fi
+ done
+ fi
+ if [[ -n "$issuer_O" ]]; then
+ issuerfinding+=" ("
+ out " ("
+ issuerfinding+="$issuer_O"
+ pr_italic "$issuer_O"
+ if [[ -n "$issuer_C" ]]; then
+ issuerfinding+=" from "
+ out " from "
+ issuerfinding+="$issuer_C"
+ pr_italic "$issuer_C"
+ fi
+ issuerfinding+=")"
+ out ")"
+ fi
+ outln
+ fileout "${jsonID}${json_postfix}" "INFO" "$issuerfinding"
+ fi
+
+ out "$indent"; pr_bold " Trust (hostname) "
+ compare_server_name_to_cert "$HOSTCERT"
+ trust_sni=$?
+
+ # Find out if the subjectAltName extension is present and contains
+ # a DNS name, since Section 6.3 of RFC 6125 says:
+ # Security Warning: A client MUST NOT seek a match for a reference
+ # identifier of CN-ID if the presented identifiers include a DNS-ID,
+ # SRV-ID, URI-ID, or any application-specific identifier types
+ # supported by the client.
+ has_dns_sans=$HAS_DNS_SANS
+
+ case $trust_sni in
+ 0) trustfinding="certificate does not match supplied URI" ;;
+ 1) trustfinding="Ok via SAN" ;;
+ 2) trustfinding="Ok via SAN wildcard" ;;
+ 4) if "$has_dns_sans"; then
+ trustfinding="via CN, but not SAN"
+ else
+ trustfinding="via CN only"
+ fi
+ ;;
+ 5) trustfinding="Ok via SAN and CN" ;;
+ 6) trustfinding="Ok via SAN wildcard and CN"
+ ;;
+ 8) if "$has_dns_sans"; then
+ trustfinding="via CN wildcard, but not SAN"
+ else
+ trustfinding="via CN (wildcard) only"
+ fi
+ ;;
+ 9) trustfinding="Ok via CN wildcard and SAN"
+ ;;
+ 10) trustfinding="Ok via SAN wildcard and CN wildcard"
+ ;;
+ esac
+
+ if [[ $trust_sni -eq 0 ]]; then
+ pr_svrty_high "$trustfinding"
+ trust_sni_finding="HIGH"
+ elif ( [[ $trust_sni -eq 4 ]] || [[ $trust_sni -eq 8 ]] ); then
+ if [[ $SERVICE == "HTTP" ]] || "$ASSUME_HTTP"; then
+ # https://bugs.chromium.org/p/chromium/issues/detail?id=308330
+ # https://bugzilla.mozilla.org/show_bug.cgi?id=1245280
+ # https://www.chromestatus.com/feature/4981025180483584
+ pr_svrty_high "$trustfinding"; out " -- Browsers are complaining"
+ trust_sni_finding="HIGH"
+ else
+ pr_svrty_medium "$trustfinding"
+ trust_sni_finding="MEDIUM"
+ # we punish CN matching for non-HTTP as it is deprecated https://tools.ietf.org/html/rfc2818#section-3.1
+ ! "$has_dns_sans" && out " -- CN only match is deprecated"
+ fi
+ else
+ pr_svrty_good "$trustfinding"
+ trust_sni_finding="OK"
+ fi
+
+ if [[ -n "$cn_nosni" ]]; then
+ compare_server_name_to_cert "$HOSTCERT.nosni"
+ trust_nosni=$?
+ has_dns_sans_nosni=$HAS_DNS_SANS
+ fi
+
+ # See issue #733.
+ if [[ -z "$sni_used" ]]; then
+ trustfinding_nosni=""
+ elif ( [[ $trust_sni -eq $trust_nosni ]] && [[ "$has_dns_sans" == "$has_dns_sans_nosni" ]] ) || \
+ ( [[ $trust_sni -eq 0 ]] && [[ $trust_nosni -eq 0 ]] ); then
+ trustfinding_nosni=" (same w/o SNI)"
+ elif [[ $trust_nosni -eq 0 ]]; then
+ if [[ $trust_sni -eq 4 ]] || [[ $trust_sni -eq 8 ]]; then
+ trustfinding_nosni=" (w/o SNI: certificate does not match supplied URI)"
+ else
+ trustfinding_nosni=" (SNI mandatory)"
+ fi
+ elif [[ $trust_nosni -eq 4 ]] || [[ $trust_nosni -eq 8 ]] || [[ $trust_sni -eq 4 ]] || [[ $trust_sni -eq 8 ]]; then
+ case $trust_nosni in
+ 1) trustfinding_nosni="(w/o SNI: Ok via SAN)" ;;
+ 2) trustfinding_nosni="(w/o SNI: Ok via SAN wildcard)" ;;
+ 4) if "$has_dns_sans_nosni"; then
+ trustfinding_nosni="(w/o SNI: via CN, but not SAN)"
+ else
+ trustfinding_nosni="(w/o SNI: via CN only)"
+ fi
+ ;;
+ 5) trustfinding_nosni="(w/o SNI: Ok via SAN and CN)" ;;
+ 6) trustfinding_nosni="(w/o SNI: Ok via SAN wildcard and CN)" ;;
+ 8) if "$has_dns_sans_nosni"; then
+ trustfinding_nosni="(w/o SNI: via CN wildcard, but not SAN)"
+ else
+ trustfinding_nosni="(w/o SNI: via CN (wildcard) only)"
+ fi
+ ;;
+ 9) trustfinding_nosni="(w/o SNI: Ok via CN wildcard and SAN)" ;;
+ 10) trustfinding_nosni="(w/o SNI: Ok via SAN wildcard and CN wildcard)" ;;
+ esac
+ elif [[ $trust_sni -ne 0 ]]; then
+ trustfinding_nosni=" (works w/o SNI)"
+ else
+ trustfinding_nosni=" (however, works w/o SNI)"
+ fi
+ if [[ -n "$sni_used" ]] || [[ $trust_nosni -eq 0 ]] || ( [[ $trust_nosni -ne 4 ]] && [[ $trust_nosni -ne 8 ]] ); then
+ outln "$trustfinding_nosni"
+ elif [[ $SERVICE == "HTTP" ]] || "$ASSUME_HTTP"; then
+ prln_svrty_high "$trustfinding_nosni"
+ else
+ prln_svrty_medium "$trustfinding_nosni"
+ fi
+
+ fileout "cert_trust${json_postfix}" "$trust_sni_finding" "${trustfinding}${trustfinding_nosni}"
+
+ out "$indent"; pr_bold " Chain of trust"; out " "
+ jsonID="cert_chain_of_trust"
+ if [[ "$issuer_O" =~ StartCom ]] || [[ "$issuer_O" =~ WoSign ]] || [[ "$issuer_CN" =~ StartCom ]] || [[ "$issuer_CN" =~ WoSign ]]; then
+ # Shortcut for this special case here.
+ pr_italic "WoSign/StartCom"; out " are " ; prln_svrty_critical "not trusted anymore (NOT ok)"
+ fileout "${jsonID}${json_postfix}" "CRITICAL" "Issuer not trusted anymore (WoSign/StartCom)"
+ else
+ # Also handles fileout, keep error if happened
+ determine_trust "$jsonID" "$json_postfix" || ((ret++))
+ fi
+
+ # https://events.ccc.de/congress/2010/Fahrplan/attachments/1777_is-the-SSLiverse-a-safe-place.pdf, see page 40pp
+ out "$indent"; pr_bold " EV cert"; out " (experimental) "
+ jsonID="cert_certificatePolicies_EV"
+ # only the first one, seldom we have two
+ policy_oid=$(awk '/ .Policy: / { print $2 }' <<< "$cert_txt" | awk 'NR < 2')
+ if grep -Eq 'Extended Validation|Extended Validated|EV SSL|EV CA' <<< "$issuer" || \
+ [[ 2.16.840.1.114028.10.1.2 == "$policy_oid" ]] || \
+ [[ 2.16.840.1.114412.1.3.0.2 == "$policy_oid" ]] || \
+ [[ 2.16.840.1.114412.2.1 == "$policy_oid" ]] || \
+ [[ 2.16.578.1.26.1.3.3 == "$policy_oid" ]] || \
+ [[ 1.3.6.1.4.1.17326.10.14.2.1.2 == "$policy_oid" ]] || \
+ [[ 1.3.6.1.4.1.17326.10.8.12.1.2 == "$policy_oid" ]] || \
+ [[ 1.3.6.1.4.1.13177.10.1.3.10 == "$policy_oid" ]] ; then
+ out "yes "
+ fileout "${jsonID}${json_postfix}" "OK" "yes"
+ else
+ out "no "
+ fileout "${jsonID}${json_postfix}" "INFO" "no"
+ fi
+ debugme echo "($(newline_to_spaces "$policy_oid"))"
+ outln
+#TODO: check browser OIDs:
+# https://mxr.mozilla.org/mozilla-central/source/security/certverifier/ExtendedValidation.cpp
+# https://chromium.googlesource.com/chromium/chromium/+/master/net/base/ev_root_ca_metadata.cc
+# https://certs.opera.com/03/ev-oids.xml
+# see #967
+
+ out "$indent"; pr_bold " ETS/\"eTLS\""
+ out ", visibility info "
+ jsonID="cert_eTLS"
+ etsi_etls_visibility_info "${jsonID}${json_postfix}" "$spaces" "$HOSTCERT" "$cert_txt"
+ # *Currently* this is even listed as a vulnerability (CWE-310, CVE-2019-919), see
+ # https://nvd.nist.gov/vuln/detail/CVE-2019-9191, https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-9191
+ # For now we leave this here. We may want to change that later or add infos to other sections (PFS & vulnerability)
+
+ out "$indent"; pr_bold " Certificate Validity (UTC) "
+ # FreeBSD + OSX can't swallow the leading blank:
+ startdate="${cert_txt#*Validity*Not Before: }"
+ startdate="${startdate%%GMT*}GMT"
+ enddate="${cert_txt#*Validity*Not Before: *Not After : }"
+ enddate="${enddate%%GMT*}GMT"
+ debugme echo "$enddate - $startdate"
+ # Now we have a normalized enddate and startdate like "Feb 27 10:03:20 2017 GMT" -- also for OpenBSD
+ if "$HAS_OPENBSDDATE"; then
+ # Best we want to do under old versions of OpenBSD, first just remove the GMT and keep start/endate for later output
+ startdate="$(parse_date "$startdate" "+%s")"
+ enddate="$(parse_date "$enddate" "+%s")"
+ # Now we extract a date block and a time block which we need for later output
+ startdate="$(parse_date "$startdate" +"%F %H:%M" "%b %d %T %Y %Z")"
+ enddate="$(parse_date "$enddate" +"%F %H:%M" "%b %d %T %Y %Z")"
+ read yearstart clockstart <<< "$startdate"
+ read yearend clockend <<< "$enddate"
+ debugme echo "$yearstart, $clockstart"
+ debugme echo "$yearend, $clockend"
+ y=$(( ${yearend:0:4} - ${yearstart:0:4} ))
+ m=$(( ${yearend:5:1} - ${yearstart:5:1} + ${yearend:6:1} - ${yearstart:6:1} ))
+ d=$(( ${yearend:8:2} - ${yearstart:8:2} ))
+ # We take the year, month, days here as old OpenBSD's date is too difficult for real conversion
+ # see comment in parse_date(). In diffseconds then we have the estimated absolute validity period
+ diffseconds=$(( d + ((m*30)) + ((y*365)) ))
+ diffseconds=$((diffseconds * 3600 * 24))
+ # Now we estimate the days left plus length of month/year:
+ yearnow="$(date -juz GMT "+%Y-%m-%d %H:%M")"
+ y=$(( ${yearend:0:4} - ${yearnow:0:4} ))
+ m=$(( ${yearend:5:1} - ${yearnow:5:1} + ${yearend:6:1} - ${yearnow:6:1} ))
+ d=$(( ${yearend:8:2} - ${yearnow:8:2} ))
+ days2expire=$(( d + ((m*30)) + ((y*365)) ))
+ else
+ startdate="$(parse_date "$startdate" +"%F %H:%M" "%b %d %T %Y %Z")"
+ enddate="$(parse_date "$enddate" +"%F %H:%M" "%b %d %T %Y %Z")"
+ days2expire=$(( $(parse_date "$enddate" "+%s" $'%F %H:%M') - $(LC_ALL=C date "+%s") )) # first in seconds
+ days2expire=$((days2expire / 3600 / 24 ))
+ diffseconds=$(( $(parse_date "$enddate" "+%s" $'%F %H:%M') - $(parse_date "$startdate" "+%s" $'%F %H:%M') ))
+ fi
+ # We adjust the thresholds by %50 for LE certificates, relaxing warnings for those certificates.
+ # . instead of \' because it does not break syntax highlighting in vim
+ if [[ "$issuer_O" =~ ^Let.s\ Encrypt ]] ; then
+ days2warn2=$((days2warn2 / 2))
+ days2warn1=$((days2warn1 / 2))
+ fi
+
+ debugme echo -n "diffseconds: $diffseconds"
+ expire=$($OPENSSL x509 -in $HOSTCERT -checkend 1 2>>$ERRFILE)
+ if ! grep -qw not <<< "$expire" ; then
+ pr_svrty_critical "expired"
+ expfinding="expired"
+ expok="CRITICAL"
+ else
+ secs2warn=$((24 * 60 * 60 * days2warn2)) # low threshold first
+ expire=$($OPENSSL x509 -in $HOSTCERT -checkend $secs2warn 2>>$ERRFILE)
+ if grep -qw not <<< "$expire"; then
+ secs2warn=$((24 * 60 * 60 * days2warn1)) # high threshold
+ expire=$($OPENSSL x509 -in $HOSTCERT -checkend $secs2warn 2>>$ERRFILE)
+ if grep -qw not <<< "$expire"; then
+ pr_svrty_good "$days2expire >= $days2warn1 days"
+ expfinding+="$days2expire >= $days2warn1 days"
+ else
+ pr_svrty_medium "expires < $days2warn1 days ($days2expire)"
+ expfinding+="expires < $days2warn1 days ($days2expire)"
+ expok="MEDIUM"
+ fi
+ else
+ pr_svrty_high "expires < $days2warn2 days ($days2expire)"
+ expfinding+="expires < $days2warn2 days ($days2expire)"
+ expok="HIGH"
+ fi
+ fi
+ outln " ($startdate --> $enddate)"
+ fileout "cert_expirationStatus${json_postfix}" "$expok" "$expfinding"
+ fileout "cert_notBefore${json_postfix}" "INFO" "$startdate" # we assume that the certificate has no start time in the future
+ fileout "cert_notAfter${json_postfix}" "$expok" "$enddate" # They are in UTC
+
+ # Internal certificates or those from appliances often have too high validity periods.
+ # We check for ~10 years and >~ 5 years
+ if [[ $diffseconds -ge $((3600 * 24 * 365 * 10)) ]]; then
+ out "$spaces"
+ prln_svrty_high ">= 10 years is way too long"
+ fileout "cert_validityPeriod${json_postfix}" "HIGH" "$((diffseconds / (3600 * 24) )) days"
+ elif [[ $diffseconds -ge $((3600 * 24 * 365 * 5)) ]]; then
+ out "$spaces"
+ prln_svrty_medium ">= 5 years is too long"
+ fileout "cert_validityPeriod${json_postfix}" "MEDIUM" "$((diffseconds / (3600 * 24) )) days"
+ elif [[ $diffseconds -ge $((3600 * 24 * 825 + 1)) ]]; then
+ # Also "official" certificates issued from March 1st, 2018 (1517353200) aren't supposed
+ # to be valid longer than 825 days which is 1517353200 in epoch seconds
+ gt_825=true
+ if "$HAS_OPENBSDDATE"; then
+ if [[ 20180301 -le ${yearstart//-/} ]]; then
+ gt_825warn=true
+ fi
+ elif [[ $(parse_date "$startdate" "+%s" $'%F %H:%M') -ge 1517353200 ]]; then
+ gt_825warn=true
+ fi
+ # Now, the verdict, depending on the issuing date
+ out "$spaces"
+ if "$gt_825warn" && "$gt_825"; then
+ prln_svrty_medium "> 825 days issued after 2018/03/01 is too long"
+ fileout "cert_validityPeriod${json_postfix}" "MEDIUM" "$((diffseconds / (3600 * 24) )) > 825 days"
+ elif "$gt_825"; then
+ outln ">= 825 days certificate life time but issued before 2018/03/01"
+ fileout "cert_validityPeriod${json_postfix}" "INFO" "$((diffseconds / (3600 * 24) )) =< 825 days"
+ fi
+ else
+ # All is fine with valididy period
+ # We ignore for now certificates < 2018/03/01. On the screen we only show debug info
+ [[ "$DEBUG" -ge 1 ]] && outln "${spaces}DEBUG: all is fine with total certificate life time"
+ fileout "cert_validityPeriod${json_postfix}" "INFO" "No finding"
+ fi
+
+ certificates_provided=1+$(grep -c "\-\-\-\-\-BEGIN CERTIFICATE\-\-\-\-\-" $TEMPDIR/intermediatecerts.pem)
+ out "$indent"; pr_bold " # of certificates provided"; out " $certificates_provided"
+ fileout "certs_countServer${json_postfix}" "INFO" "${certificates_provided}"
+ if "$certificate_list_ordering_problem"; then
+ prln_svrty_low " (certificate list ordering problem)"
+ fileout "certs_list_ordering_problem${json_postfix}" "LOW" "yes"
+ else
+ fileout "certs_list_ordering_problem${json_postfix}" "INFO" "no"
+ outln
+ fi
+
+ if "$PHONE_OUT"; then
+ out "$indent"; pr_bold " In pwnedkeys.com DB "
+ check_pwnedkeys "$HOSTCERT" "$cert_key_algo" "$cert_keysize"
+ case "$?" in
+ 0) outln "not checked"; fileout "pwnedkeys${json_postfix}" "INFO" "not checked" ;;
+ 1) outln "not in database"; fileout "pwnedkeys${json_postfix}" "INFO" "not in database" ;;
+ 2) pr_svrty_critical "NOT ok --"; outln " key appears in database"; fileout "pwnedkeys${json_postfix}" "CRITICAL" "private key is known" ;;
+ 7) prln_warning "error querying https://v1.pwnedkeys.com"; fileout "pwnedkeys${json_postfix}" "WARN" "connection error" ;;
+ esac
+ fi
+
+ out "$indent"; pr_bold " Certificate Revocation List "
+ jsonID="cert_crlDistributionPoints"
+ # ~ get next 50 lines after pattern , strip until Signature Algorithm and retrieve URIs
+ crl="$(awk '/X509v3 CRL Distribution/{i=50} i&&i--' <<< "$cert_txt" | awk '/^$/,/^ [a-zA-Z0-9]+|^ Signature Algorithm:/' | awk -F'URI:' '/URI/ { print $2 }')"
+ if [[ -z "$crl" ]] ; then
+ fileout "${jsonID}${json_postfix}" "INFO" "--"
+ outln "--"
+ else
+ if [[ $(count_lines "$crl") -eq 1 ]]; then
+ out "$crl"
+ if [[ "$expfinding" != "expired" ]]; then
+ check_revocation_crl "$crl" "cert_crlRevoked${json_postfix}"
+ ret=$((ret +$?))
+ fi
+ outln
+ else # more than one CRL
+ first_crl=true
+ while read -r line; do
+ if "$first_crl"; then
+ first_crl=false
+ else
+ out "$spaces"
+ fi
+ out "$line"
+ if [[ "$expfinding" != expired ]]; then
+ check_revocation_crl "$line" "cert_crlRevoked${json_postfix}"
+ ret=$((ret +$?))
+ fi
+ outln
+ done <<< "$crl"
+ fi
+ fileout "${jsonID}${json_postfix}" "INFO" "$crl"
+ fi
+
+ out "$indent"; pr_bold " OCSP URI "
+ jsonID="cert_ocspURL"
+ ocsp_uri=$($OPENSSL x509 -in $HOSTCERT -noout -ocsp_uri 2>>$ERRFILE)
+ if [[ -z "$ocsp_uri" ]]; then
+ outln "--"
+ fileout "${jsonID}${json_postfix}" "INFO" "--"
+ else
+ if [[ $(count_lines "$ocsp_uri") -eq 1 ]]; then
+ out "$ocsp_uri"
+ if [[ "$expfinding" != "expired" ]]; then
+ check_revocation_ocsp "$ocsp_uri" "" "cert_ocspRevoked${json_postfix}"
+ fi
+ ret=$((ret +$?))
+ outln
+ else
+ first_ocsp=true
+ while read -r line; do
+ if "$first_ocsp"; then
+ first_ocsp=false
+ else
+ out "$spaces"
+ fi
+ out "$line"
+ if [[ "$expfinding" != "expired" ]]; then
+ check_revocation_ocsp "$line" "" "cert_ocspRevoked${json_postfix}"
+ ret=$((ret +$?))
+ fi
+ outln
+ done <<< "$ocsp_uri"
+ fi
+ fileout "${jsonID}${json_postfix}" "INFO" "$ocsp_uri"
+ fi
+ if [[ -z "$ocsp_uri" ]] && [[ -z "$crl" ]]; then
+ out "$spaces"
+ pr_svrty_high "NOT ok --"
+ outln " neither CRL nor OCSP URI provided"
+ fileout "cert_revocation${json_postfix}" "HIGH" "Neither CRL nor OCSP URI provided"
+ fi
+
+ out "$indent"; pr_bold " OCSP stapling "
+ jsonID="OCSP_stapling"
+ if grep -a "OCSP response" <<< "$ocsp_response" | grep -q "no response sent" ; then
+ if [[ -n "$ocsp_uri" ]]; then
+ pr_svrty_low "not offered"
+ fileout "${jsonID}${json_postfix}" "LOW" "not offered"
+ else
+ out "not offered"
+ fileout "${jsonID}${json_postfix}" "INFO" "not offered"
+ fi
+ else
+ if grep -a "OCSP Response Status" <<< "$ocsp_response_status" | grep -q successful; then
+ pr_svrty_good "offered"
+ fileout "${jsonID}${json_postfix}" "OK" "offered"
+ provides_stapling=true
+ check_revocation_ocsp "" "$ocsp_response_binary" "cert_ocspRevoked${json_postfix}"
+ elif [[ "$ocsp_response" =~ Responder\ Error: ]]; then
+ response="$(awk '/Responder Error:/ { print $3 }' <<< "$ocsp_response")"
+ pr_warning "stapled OCSP response contained an error response from OCSP responder: $response"
+ fileout "${jsonID}${json_postfix}" "WARN" "stapled OCSP response contained an error response from OCSP responder: $response"
+ else
+ if $GOST_STATUS_PROBLEM; then
+ pr_warning "(GOST servers make problems here, sorry)"
+ fileout "${jsonID}${json_postfix}" "WARN" "(The GOST server made a problem here, sorry)"
+ ((ret++))
+ else
+ out "(response status unknown)"
+ fileout "${jsonID}${json_postfix}" "OK" " not sure what's going on here, '$ocsp_response'"
+ debugme grep -a -A20 -B2 "OCSP response" <<<"$ocsp_response"
+ ((ret++))
+ fi
+ fi
+ fi
+ outln
+
+ out "$indent"; pr_bold " OCSP must staple extension ";
+ must_staple "$json_postfix" "$provides_stapling" "$cert_txt"
+
+ out "$indent"; pr_bold " DNS CAA RR"; out " (experimental) "
+ jsonID="DNS_CAArecord"
+ caa_node="$NODE"
+ caa=""
+ while ( [[ -z "$caa" ]] && [[ ! -z "$caa_node" ]] ); do
+ caa="$(get_caa_rr_record $caa_node)"
+ [[ $caa_node =~ '.'$ ]] || caa_node+="."
+ caa_node=${caa_node#*.}
+ done
+ if [[ -n "$caa" ]]; then
+ pr_svrty_good "available"; out " - please check for match with \"Issuer\" above"
+ if [[ $(count_lines "$caa") -eq 1 ]]; then
+ out ": "
+ else
+ outln; out "$spaces"
+ fi
+ while read caa; do
+ if [[ -n "$caa" ]]; then
+ all_caa+="$caa, "
+ fi
+ done <<< "$caa"
+ all_caa=${all_caa%, } # strip trailing comma
+ pr_italic "$(out_row_aligned_max_width "$all_caa" "$indent " $TERM_WIDTH)"
+ fileout "${jsonID}${json_postfix}" "OK" "$all_caa"
+ elif [[ -n "$NODNS" ]]; then
+ out "(instructed to minimize DNS queries)"
+ fileout "${jsonID}${json_postfix}" "INFO" "check skipped as instructed"
+ else
+ pr_svrty_low "not offered"
+ fileout "${jsonID}${json_postfix}" "LOW" "--"
+ fi
+ outln
+
+ out "$indent"; pr_bold " Certificate Transparency ";
+ jsonID="certificate_transparency"
+ if [[ "$ct" =~ extension ]]; then
+ pr_svrty_good "yes"; outln " ($ct)"
+ fileout "${jsonID}${json_postfix}" "OK" "yes ($ct)"
+ else
+ outln "$ct"
+ fileout "${jsonID}${json_postfix}" "INFO" "$ct"
+ fi
+ outln
+ return $ret
+}
+
+run_server_defaults() {
+ local ciph newhostcert sni
+ local match_found
+ local sessticket_lifetime_hint="" sessticket_proto="" lifetime unit
+ local -i i n
+ local -i certs_found=0
+ local -i ret=0
+ local -a previous_hostcert previous_hostcert_txt previous_hostcert_type
+ local -a previous_hostcert_issuer previous_intermediates previous_ordering_problem keysize cipher
+ local -a ocsp_response_binary ocsp_response ocsp_response_status sni_used tls_version ct
+ local -a ciphers_to_test certificate_type
+ local -a -i success
+ local cn_nosni cn_sni sans_nosni sans_sni san tls_extensions
+ local using_sockets=true
+
+ "$SSL_NATIVE" && using_sockets=false
+
+ # Try each public key type once:
+ # ciphers_to_test[1]: cipher suites using certificates with RSA signature public keys
+ # ciphers_to_test[2]: cipher suites using certificates with RSA key encipherment public keys
+ # ciphers_to_test[3]: cipher suites using certificates with DSA signature public keys
+ # ciphers_to_test[4]: cipher suites using certificates with DH key agreement public keys
+ # ciphers_to_test[5]: cipher suites using certificates with ECDH key agreement public keys
+ # ciphers_to_test[6]: cipher suites using certificates with ECDSA signature public keys
+ # ciphers_to_test[7]: cipher suites using certificates with GOST R 34.10 (either 2001 or 94) public keys
+ ciphers_to_test[1]="aRSA:eRSA"
+ ciphers_to_test[2]=""
+ ciphers_to_test[3]="aDSS:aDH:aECDH:aECDSA:aGOST"
+ ciphers_to_test[4]=""
+ ciphers_to_test[5]=""
+ ciphers_to_test[6]=""
+ ciphers_to_test[7]=""
+ ciphers_to_test[8]="tls1_3_RSA"
+ ciphers_to_test[9]="tls1_3_ECDSA"
+ certificate_type[1]="" ; certificate_type[2]=""
+ certificate_type[3]=""; certificate_type[4]=""
+ certificate_type[5]="" ; certificate_type[6]=""
+ certificate_type[7]="" ; certificate_type[8]="RSASig"
+ certificate_type[9]="ECDSA"
+
+ for (( n=1; n <= 16 ; n++ )); do
+ # Some servers use a different certificate if the ClientHello
+ # specifies TLSv1.1 and doesn't include a server name extension.
+ # So, for each public key type for which a certificate was found,
+ # try again, but only with TLSv1.1 and without SNI.
+ if [[ $n -ne 1 ]] && [[ "$OPTIMAL_PROTO" == -ssl2 ]]; then
+ ciphers_to_test[n]=""
+ elif [[ $n -ge 10 ]]; then
+ ciphers_to_test[n]=""
+ [[ ${success[n-9]} -eq 0 ]] && [[ $(has_server_protocol "tls1_1") -ne 1 ]] && \
+ ciphers_to_test[n]="${ciphers_to_test[n-9]}" && certificate_type[n]="${certificate_type[n-9]}"
+ fi
+
+ if [[ -n "${ciphers_to_test[n]}" ]]; then
+ if [[ $n -ge 10 ]]; then
+ sni="$SNI"
+ SNI=""
+ get_server_certificate "${ciphers_to_test[n]}" "tls1_1"
+ success[n]=$?
+ SNI="$sni"
+ else
+ get_server_certificate "${ciphers_to_test[n]}"
+ success[n]=$?
+ fi
+ if [[ ${success[n]} -eq 0 ]] && [[ -s "$HOSTCERT" ]]; then
+ [[ $n -ge 10 ]] && [[ ! -e $HOSTCERT.nosni ]] && cp $HOSTCERT $HOSTCERT.nosni
+ cp "$TEMPDIR/$NODEIP.get_server_certificate.txt" $TMPFILE
+ >$ERRFILE
+ if [[ -z "$sessticket_lifetime_hint" ]]; then
+ sessticket_lifetime_hint=$(awk '/session ticket life/ { if (!found) print; found=1 }' $TMPFILE)
+ sessticket_proto="$(get_protocol "$TMPFILE")"
+ fi
+
+ if [[ $n -le 7 ]]; then
+ ciph="$(get_cipher $TMPFILE)"
+ if [[ "$ciph" != TLS_* ]] && [[ "$ciph" != SSL_* ]]; then
+ ciph="$(openssl2rfc "$ciph")"
+ fi
+ if [[ "$ciph" == TLS_DHE_RSA_* ]] || [[ "$ciph" == TLS_ECDHE_RSA_* ]] || [[ "$ciph" == TLS_CECPQ1_RSA_* ]]; then
+ certificate_type[n]="RSASig"
+ if [[ -z "${ciphers_to_test[n+1]}" ]]; then
+ ciphers_to_test[n+1]="${ciphers_to_test[n]/aRSA/}"
+ ciphers_to_test[n+1]="${ciphers_to_test[n+1]/::/:}"
+ [[ "${ciphers_to_test[n+1]:0:1}" == : ]] && ciphers_to_test[n+1]="${ciphers_to_test[n+1]:1}"
+ fi
+ ciphers_to_test[n]="aRSA"
+ elif [[ "$ciph" == TLS_RSA_* ]] || [[ "$ciph" == SSL_* ]] || [[ "$ciph" == TLS_GOST*_RSA_* ]]; then
+ certificate_type[n]="RSAKMK"
+ if [[ -z "${ciphers_to_test[n+1]}" ]]; then
+ ciphers_to_test[n+1]="${ciphers_to_test[n]/eRSA/}"
+ ciphers_to_test[n+1]="${ciphers_to_test[n+1]/::/:}"
+ [[ "${ciphers_to_test[n+1]:0:1}" == : ]] && ciphers_to_test[n+1]="${ciphers_to_test[n+1]:1}"
+ fi
+ ciphers_to_test[n]="eRSA"
+ elif [[ "$ciph" == TLS_DHE_DSS_* ]]; then
+ certificate_type[n]="DSA"
+ if [[ -z "${ciphers_to_test[n+1]}" ]]; then
+ ciphers_to_test[n+1]="${ciphers_to_test[n]/aDSS/}"
+ ciphers_to_test[n+1]="${ciphers_to_test[n+1]/::/:}"
+ [[ "${ciphers_to_test[n+1]:0:1}" == : ]] && ciphers_to_test[n+1]="${ciphers_to_test[n+1]:1}"
+ fi
+ ciphers_to_test[n]="aDSS"
+ elif [[ "$ciph" == TLS_DH_* ]]; then
+ certificate_type[n]="DH"
+ if [[ -z "${ciphers_to_test[n+1]}" ]]; then
+ ciphers_to_test[n+1]="${ciphers_to_test[n]/aDH/}"
+ ciphers_to_test[n+1]="${ciphers_to_test[n+1]/::/:}"
+ [[ "${ciphers_to_test[n+1]:0:1}" == : ]] && ciphers_to_test[n+1]="${ciphers_to_test[n+1]:1}"
+ fi
+ ciphers_to_test[n]="aDH"
+ elif [[ "$ciph" == TLS_ECDH_* ]]; then
+ certificate_type[n]="ECDH"
+ if [[ -z "${ciphers_to_test[n+1]}" ]]; then
+ ciphers_to_test[n+1]="${ciphers_to_test[n]/aECDH/}"
+ ciphers_to_test[n+1]="${ciphers_to_test[n+1]/::/:}"
+ [[ "${ciphers_to_test[n+1]:0:1}" == : ]] && ciphers_to_test[n+1]="${ciphers_to_test[n+1]:1}"
+ fi
+ ciphers_to_test[n]="aECDH"
+ elif [[ "$ciph" == TLS_ECDHE_ECDSA_* ]] || [[ "$ciph" == TLS_CECPQ1_ECDSA_* ]]; then
+ certificate_type[n]="ECDSA"
+ if [[ -z "${ciphers_to_test[n+1]}" ]]; then
+ ciphers_to_test[n+1]="${ciphers_to_test[n]/aECDSA/}"
+ ciphers_to_test[n+1]="${ciphers_to_test[n+1]/::/:}"
+ [[ "${ciphers_to_test[n+1]:0:1}" == : ]] && ciphers_to_test[n+1]="${ciphers_to_test[n+1]:1}"
+ fi
+ ciphers_to_test[n]="aECDSA"
+ elif [[ "$ciph" == TLS_GOST* ]]; then
+ certificate_type[n]="GOST"
+ if [[ -z "${ciphers_to_test[n+1]}" ]]; then
+ ciphers_to_test[n+1]="${ciphers_to_test[n]/aGOST/}"
+ ciphers_to_test[n+1]="${ciphers_to_test[n+1]/::/:}"
+ [[ "${ciphers_to_test[n+1]:0:1}" == : ]] && ciphers_to_test[n+1]="${ciphers_to_test[n+1]:1}"
+ fi
+ ciphers_to_test[n]="aGOST"
+ fi
+ fi
+ # check whether the host's certificate has been seen before
+ match_found=false
+ i=1
+ newhostcert=$(cat $HOSTCERT)
+ while [[ $i -le $certs_found ]]; do
+ if [[ "$newhostcert" == "${previous_hostcert[i]}" ]]; then
+ match_found=true
+ break;
+ fi
+ i=$((i + 1))
+ done
+ if ! "$match_found" && [[ $n -ge 10 ]] && [[ $certs_found -ne 0 ]]; then
+ # A new certificate was found using TLSv1.1 without SNI.
+ # Check to see if the new certificate should be displayed.
+ # It should be displayed if it is either a match for the
+ # $NODE being tested or if it has the same subject
+ # (CN and SAN) as other certificates for this host.
+ compare_server_name_to_cert "$HOSTCERT"
+ [[ $? -ne 0 ]] && success[n]=0 || success[n]=1
+
+ if [[ ${success[n]} -ne 0 ]]; then
+ cn_nosni="$(toupper "$(get_cn_from_cert $HOSTCERT)")"
+ sans_nosni="$(toupper "$(get_san_dns_from_cert "$HOSTCERT")")"
+
+ echo "${previous_hostcert[1]}" > $HOSTCERT
+ cn_sni="$(toupper "$(get_cn_from_cert $HOSTCERT)")"
+
+ # FIXME: Not sure what the matching rule should be. At
+ # the moment, the no SNI certificate is considered a
+ # match if the CNs are the same and the SANs (if
+ # present) contain at least one DNS name in common.
+ if [[ "$cn_nosni" == "$cn_sni" ]]; then
+ sans_sni="$(toupper "$(get_san_dns_from_cert "$HOSTCERT")")"
+ if [[ "$sans_nosni" == "$sans_sni" ]]; then
+ success[n]=0
+ else
+ while read -r san; do
+ [[ -n "$san" ]] && [[ " $sans_sni " =~ " $san " ]] && success[n]=0 && break
+ done <<< "$sans_nosni"
+ fi
+ fi
+ fi
+ # If the certificate found for TLSv1.1 w/o SNI appears to
+ # be for a different host, then set match_found to true so
+ # that the new certificate will not be included in the output.
+ [[ ${success[n]} -ne 0 ]] && match_found=true
+ fi
+ if ! "$match_found"; then
+ certs_found=$(( certs_found + 1))
+ cipher[certs_found]=${ciphers_to_test[n]}
+ keysize[certs_found]=$(awk '/Server public key/ { print $(NF-1) }' $TMPFILE)
+ # If an OCSP response was sent, then get the full
+ # response so that certificate_info() can determine
+ # whether it includes a certificate transparency extension.
+ ocsp_response_binary[certs_found]="$STAPLED_OCSP_RESPONSE"
+ if grep -a "OCSP response:" $TMPFILE | grep -q "no response sent"; then
+ ocsp_response[certs_found]="$(grep -a "OCSP response" $TMPFILE)"
+ else
+ ocsp_response[certs_found]="$(awk -v n=2 '/OCSP response:/ {start=1; inc=2} /======================================/ { if (start) {inc--} } inc' $TMPFILE)"
+ fi
+ ocsp_response_status[certs_found]=$(grep -a "OCSP Response Status" $TMPFILE)
+ previous_hostcert[certs_found]=$newhostcert
+ previous_hostcert_txt[certs_found]="$($OPENSSL x509 -noout -text 2>>$ERRFILE <<< "$newhostcert")"
+ previous_intermediates[certs_found]=$(cat $TEMPDIR/intermediatecerts.pem)
+ previous_hostcert_issuer[certs_found]=""
+ [[ -n "${previous_intermediates[certs_found]}" ]] && [[ -r $TEMPDIR/hostcert_issuer.pem ]] && \
+ previous_hostcert_issuer[certs_found]=$(cat $TEMPDIR/hostcert_issuer.pem)
+ previous_ordering_problem[certs_found]=$CERTIFICATE_LIST_ORDERING_PROBLEM
+ [[ $n -ge 10 ]] && sni_used[certs_found]="" || sni_used[certs_found]="$SNI"
+ tls_version[certs_found]="$DETECTED_TLS_VERSION"
+ previous_hostcert_type[certs_found]=" ${certificate_type[n]}"
+ if [[ $DEBUG -ge 1 ]]; then
+ echo "${previous_hostcert[certs_found]}" > $TEMPDIR/host_certificate_$certs_found.pem
+ echo "${previous_hostcert_txt[certs_found]}" > $TEMPDIR/host_certificate_$certs_found.txt
+ fi
+ else
+ previous_hostcert_type[i]+=" ${certificate_type[n]}"
+ fi
+ fi
+ fi
+ done
+
+ determine_tls_extensions
+
+ if [[ $? -eq 0 ]] && [[ "$OPTIMAL_PROTO" != -ssl2 ]]; then
+ cp "$TEMPDIR/$NODEIP.determine_tls_extensions.txt" $TMPFILE
+ >$ERRFILE
+ if [[ -z "$sessticket_lifetime_hint" ]]; then
+ sessticket_lifetime_hint=$(awk '/session ticket lifetime/ { if (!found) print; found=1 }' $TMPFILE)
+ sessticket_proto="$(get_protocol "$TMPFILE")"
+ fi
+ fi
+ if "$using_sockets" && ! "$TLS13_ONLY" && [[ -z "$sessticket_lifetime_hint" ]] && [[ "$OPTIMAL_PROTO" != -ssl2 ]]; then
+ if "$HAS_TLS13" && ( [[ -z "$OPTIMAL_PROTO" ]] || [[ "$OPTIMAL_PROTO" == -tls1_3 ]] ) ; then
+ # If a session ticket were sent in response to a TLSv1.3 ClientHello, then a session ticket
+ # would have been found by get_server_certificate(). So, try again with a TLSv1.2 ClientHello.
+ $OPENSSL s_client $(s_client_options "$STARTTLS $BUGS -no_tls1_3 -connect $NODEIP:$PORT $PROXY $SNI") </dev/null 2>$ERRFILE >$TMPFILE
+ else
+ $OPENSSL s_client $(s_client_options "$STARTTLS $BUGS "$OPTIMAL_PROTO" -connect $NODEIP:$PORT $PROXY $SNI") </dev/null 2>$ERRFILE >$TMPFILE
+ fi
+ if sclient_connect_successful $? $TMPFILE; then
+ sessticket_lifetime_hint=$(awk '/session ticket lifetime/ { if (!found) print; found=1 }' $TMPFILE)
+ sessticket_proto="$(get_protocol "$TMPFILE")"
+ fi
+ fi
+ [[ -z "$sessticket_lifetime_hint" ]] && TLS_TICKETS=false || TLS_TICKETS=true
+
+ debugme echo "# certificates found $certs_found"
+ # Now that all of the server's certificates have been found, determine for
+ # each certificate whether certificate transparency information is provided.
+ for (( i=1; i <= certs_found; i++ )); do
+ ct[i]="$(certificate_transparency "${previous_hostcert_txt[i]}" "${ocsp_response[i]}" "$certs_found" "${cipher[i]}" "${sni_used[i]}" "${tls_version[i]}")"
+ # If certificate_transparency() called tls_sockets() and found a "signed certificate timestamps" extension,
+ # then add it to $TLS_EXTENSIONS, since it may not have been found by determine_tls_extensions().
+ [[ $certs_found -gt 1 ]] && [[ "${ct[i]}" == TLS\ extension ]] && extract_new_tls_extensions "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt"
+ done
+
+ outln
+ pr_headlineln " Testing server defaults (Server Hello) "
+ outln
+
+ pr_bold " TLS extensions (standard) "
+ if [[ -z "$TLS_EXTENSIONS" ]]; then
+ outln "(none)"
+ fileout "TLS_extensions" "INFO" "(none)"
+ else
+#FIXME: we rather want to have the chance to print each ext in italics or another format.
+# Atm is a string of quoted strings -- that needs to be fixed at the root then
+ # out_row_aligned_max_width() places line breaks at space characters.
+ # So, in order to prevent the text for an extension from being broken
+ # across lines, temporarily replace space characters within the text
+ # of an extension with "}", and then convert the "}" back to space in
+ # the output of out_row_aligned_max_width().
+ tls_extensions="${TLS_EXTENSIONS// /{}"
+ tls_extensions="${tls_extensions//\"{\"/\" \"}"
+ tls_extensions="$(out_row_aligned_max_width "$tls_extensions" " " $TERM_WIDTH)"
+ tls_extensions="${tls_extensions//{/ }"
+ outln "$tls_extensions"
+ fileout "TLS_extensions" "INFO" "$TLS_EXTENSIONS"
+ fi
+
+ pr_bold " Session Ticket RFC 5077 hint "
+ jsonID="TLS_session_ticket"
+ if [[ -z "$sessticket_lifetime_hint" ]]; then
+ outln "no -- no lifetime advertised"
+ fileout "${jsonID}" "INFO" "no -- no lifetime advertised"
+ # it MAY be given a hint of the lifetime of the ticket, see https://tools.ietf.org/html/rfc5077#section-5.6 .
+ # Sometimes it just does not -- but it then may also support TLS session tickets reuse
+ else
+ lifetime=$(grep -a lifetime <<< "$sessticket_lifetime_hint" | sed 's/[A-Za-z:() ]//g')
+ unit=$(grep -a lifetime <<< "$sessticket_lifetime_hint" | sed -e 's/^.*'"$lifetime"'//' -e 's/[ ()]//g')
+ out "$lifetime $unit"
+ if [[ $((3600 * 24)) -lt $lifetime ]]; then
+ prln_svrty_low " but: PFS requires session ticket keys to be rotated < daily !"
+ fileout "$jsonID" "LOW" "valid for $lifetime $unit (>daily)"
+ else
+ outln ", session tickets keys seems to be rotated < daily"
+ fileout "$jsonID" "INFO" "valid for $lifetime $unit only (<daily)"
+ fi
+ fi
+
+ pr_bold " SSL Session ID support "
+ jsonID="SSL_sessionID_support"
+ if "$NO_SSL_SESSIONID"; then
+ outln "no"
+ fileout "$jsonID" "INFO" "no"
+ else
+ outln "yes"
+ fileout "$jsonID" "INFO" "yes"
+ fi
+
+ pr_bold " Session Resumption "
+ jsonID="sessionresumption_ticket"
+ sub_session_resumption "$sessticket_proto"
+ case $? in
+ 0) SESS_RESUMPTION[2]="ticket=yes"
+ out "Tickets: yes, "
+ fileout "$jsonID" "INFO" "supported"
+ ;;
+ 1) SESS_RESUMPTION[2]="ticket=no"
+ out "Tickets no, "
+ fileout "$jsonID" "INFO" "not supported"
+ ;;
+ 5) SESS_RESUMPTION[2]="ticket=noclue"
+ pr_warning "Ticket resumption test failed, pls report / "
+ fileout "$jsonID" "WARN" "check failed, pls report"
+ ((ret++))
+ ;;
+ 6) SESS_RESUMPTION[2]="ticket=clientauth"
+ pr_warning "Client Auth: Ticket resumption test not supported / "
+ fileout "$jsonID" "WARN" "check couldn't be performed because of client authentication"
+ ;;
+ 7) SESS_RESUMPTION[2]="ticket=unsuccessful"
+ pr_warning "Connect problem: Ticket resumption test not possible / "
+ fileout "$jsonID" "WARN" "check failed because of connect problem"
+ ((ret++))
+ ;;
+ esac
+
+ jsonID="sessionresumption_ID"
+ if "$NO_SSL_SESSIONID"; then
+ SESS_RESUMPTION[1]="ID=no"
+ outln "ID: no"
+ fileout "$jsonID" "INFO" "No Session ID, no resumption"
+ else
+ sub_session_resumption "$sessticket_proto" ID
+ case $? in
+ 0) SESS_RESUMPTION[1]="ID=yes"
+ outln "ID: yes"
+ fileout "$jsonID" "INFO" "supported"
+ ;;
+ 1|2) SESS_RESUMPTION[1]="ID=no"
+ outln "ID: no"
+ fileout "$jsonID" "INFO" "not supported"
+ ;;
+ 5) SESS_RESUMPTION[1]="ID=noclue"
+ prln_warning "ID resumption test failed, pls report"
+ fileout "$jsonID" "WARN" "check failed, pls report"
+ ((ret++))
+ ;;
+ 6) SESS_RESUMPTION[1]="ID=clientauth"
+ # [[ ${SESS_RESUMPTION[2]} =~ clientauth ]] || pr_warning "Client Auth: "
+ prln_warning "Client Auth: ID resumption test not supported"
+ fileout "$jsonID" "WARN" "check couldn't be performed because of client authentication"
+ ;;
+ 7) SESS_RESUMPTION[1]="ID=unsuccessful"
+ prln_warning "ID resumption test failed"
+ fileout "$jsonID" "WARN" "check failed because of connect problem"
+ ((ret++))
+ ;;
+ esac
+ fi
+
+ tls_time
+
+ if [[ -n "$SNI" ]] && [[ $certs_found -ne 0 ]] && [[ ! -e $HOSTCERT.nosni ]]; then
+ # no cipher suites specified here. We just want the default vhost subject
+ if ! "$HAS_TLS13" && [[ $(has_server_protocol "tls1_3") -eq 0 ]]; then
+ sni="$SNI" ; SNI=""
+ mv $HOSTCERT $HOSTCERT.save
+ # Send same list of cipher suites as OpenSSL 1.1.1 sends (but with
+ # all 5 TLSv1.3 ciphers offered.
+ tls_sockets "04" \
+ "c0,2c, c0,30, 00,9f, cc,a9, cc,a8, cc,aa, c0,2b, c0,2f,
+ 00,9e, c0,24, c0,28, 00,6b, c0,23, c0,27, 00,67, c0,0a,
+ c0,14, 00,39, c0,09, c0,13, 00,33, 00,9d, 00,9c, 13,02,
+ 13,03, 13,01, 13,04, 13,05, 00,3d, 00,3c, 00,35, 00,2f,
+ 00,ff" \
+ "all+"
+ success[0]=$?
+ if [[ ${success[0]} -eq 0 ]] || [[ ${success[0]} -eq 2 ]]; then
+ if [[ -s $HOSTCERT ]]; then
+ mv $HOSTCERT $HOSTCERT.nosni
+ else
+ # The connection was successful, but the certificate could
+ # not be obtained (probably because the connection was TLS 1.3
+ # and $OPENSSL does not support the key exchange group that was
+ # selected). So, try again using OpenSSL (which will not use a TLS 1.3
+ # ClientHello).
+ $OPENSSL s_client $(s_client_options "$STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY $OPTIMAL_PROTO") 2>>$ERRFILE </dev/null | \
+ awk '/-----BEGIN/,/-----END/ { print $0 }' >$HOSTCERT.nosni
+ fi
+ else
+ >$HOSTCERT.nosni
+ fi
+ mv $HOSTCERT.save $HOSTCERT
+ SNI="$sni"
+ else
+ $OPENSSL s_client $(s_client_options "$STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY $OPTIMAL_PROTO") 2>>$ERRFILE </dev/null | \
+ awk '/-----BEGIN/,/-----END/ { print $0 }' >$HOSTCERT.nosni
+ fi
+ elif [[ $certs_found -eq 0 ]] && [[ -s "$HOSTCERT" ]]; then
+ outln
+ generic_nonfatal "Client problem, shouldn't happen: Host certificate found but we can't continue with \"server defaults\"."
+ elif [[ $certs_found -eq 0 ]]; then
+ outln
+ if $TLS13_ONLY; then
+ generic_nonfatal "Client problem: We need openssl supporting TLS 1.3. We can't continue with \"server defaults\" as we cannot retrieve the certificate. "
+ else
+ generic_nonfatal "Client problem, No server cerificate could be retrieved. Thus we can't continue with \"server defaults\"."
+ fi
+ fi
+ [[ $DEBUG -ge 1 ]] && [[ -e $HOSTCERT.nosni ]] && $OPENSSL x509 -in $HOSTCERT.nosni -text -noout 2>>$ERRFILE > $HOSTCERT.nosni.txt
+
+ fileout "cert_numbers" "INFO" "$certs_found"
+ for (( i=1; i <= certs_found; i++ )); do
+ echo "${previous_hostcert[i]}" > $HOSTCERT
+ echo "${previous_intermediates[i]}" > $TEMPDIR/intermediatecerts.pem
+ echo "${previous_hostcert_issuer[i]}" > $TEMPDIR/hostcert_issuer.pem
+ certificate_info "$i" "$certs_found" "${previous_hostcert_txt[i]}" \
+ "${cipher[i]}" "${keysize[i]}" "${previous_hostcert_type[i]}" \
+ "${ocsp_response_binary[i]}" "${ocsp_response[i]}" \
+ "${ocsp_response_status[i]}" "${sni_used[i]}" "${ct[i]}" \
+ "${previous_ordering_problem[i]}"
+ [[ $? -ne 0 ]] && ((ret++))
+ done
+ return $ret
+}
+
+get_session_ticket_lifetime_from_serverhello() {
+ awk '/session ticket.*lifetime/ { print $(NF-1) "$1" }'
+}
+
+get_san_dns_from_cert() {
+ echo "$($OPENSSL x509 -in "$1" -noout -text 2>>$ERRFILE | \
+ grep -A2 "Subject Alternative Name" | tr ',' '\n' | grep "DNS:" | \
+ sed -e 's/DNS://g' -e 's/ //g')"
+}
+
+
+run_pfs() {
+ local -i sclient_success
+ local pfs_offered=false ecdhe_offered=false ffdhe_offered=false
+ local pfs_tls13_offered=false
+ local protos_to_try proto hexc dash pfs_cipher sslvers auth mac export curve dhlen
+ local -a hexcode normalized_hexcode ciph rfc_ciph kx enc ciphers_found sigalg ossl_supported
+ # generated from 'kEECDH:kEDH:!aNULL:!eNULL:!DES:!3DES:!RC4' with openssl 1.0.2i and openssl 1.1.0
+ local pfs_cipher_list="DHE-DSS-AES128-GCM-SHA256:DHE-DSS-AES128-SHA256:DHE-DSS-AES128-SHA:DHE-DSS-AES256-GCM-SHA384:DHE-DSS-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-DSS-CAMELLIA128-SHA256:DHE-DSS-CAMELLIA128-SHA:DHE-DSS-CAMELLIA256-SHA256:DHE-DSS-CAMELLIA256-SHA:DHE-DSS-SEED-SHA:DHE-RSA-AES128-CCM8:DHE-RSA-AES128-CCM:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-CCM8:DHE-RSA-AES256-CCM:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-CAMELLIA128-SHA256:DHE-RSA-CAMELLIA128-SHA:DHE-RSA-CAMELLIA256-SHA256:DHE-RSA-CAMELLIA256-SHA:DHE-RSA-CHACHA20-POLY1305-OLD:DHE-RSA-CHACHA20-POLY1305:DHE-RSA-SEED-SHA:ECDHE-ECDSA-AES128-CCM8:ECDHE-ECDSA-AES128-CCM:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-ECDSA-AES256-CCM8:ECDHE-ECDSA-AES256-CCM:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-ECDSA-CAMELLIA128-SHA256:ECDHE-ECDSA-CAMELLIA256-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305-OLD:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-RSA-CAMELLIA128-SHA256:ECDHE-RSA-CAMELLIA256-SHA384:ECDHE-RSA-CHACHA20-POLY1305-OLD:ECDHE-RSA-CHACHA20-POLY1305"
+ local pfs_hex_cipher_list="" ciphers_to_test tls13_ciphers_to_test
+ local ecdhe_cipher_list="" tls13_cipher_list="" ecdhe_cipher_list_hex="" ffdhe_cipher_list_hex=""
+ local curves_hex=("00,01" "00,02" "00,03" "00,04" "00,05" "00,06" "00,07" "00,08" "00,09" "00,0a" "00,0b" "00,0c" "00,0d" "00,0e" "00,0f" "00,10" "00,11" "00,12" "00,13" "00,14" "00,15" "00,16" "00,17" "00,18" "00,19" "00,1a" "00,1b" "00,1c" "00,1d" "00,1e")
+ local -a curves_ossl=("sect163k1" "sect163r1" "sect163r2" "sect193r1" "sect193r2" "sect233k1" "sect233r1" "sect239k1" "sect283k1" "sect283r1" "sect409k1" "sect409r1" "sect571k1" "sect571r1" "secp160k1" "secp160r1" "secp160r2" "secp192k1" "prime192v1" "secp224k1" "secp224r1" "secp256k1" "prime256v1" "secp384r1" "secp521r1" "brainpoolP256r1" "brainpoolP384r1" "brainpoolP512r1" "X25519" "X448")
+ local -a curves_ossl_output=("K-163" "sect163r1" "B-163" "sect193r1" "sect193r2" "K-233" "B-233" "sect239k1" "K-283" "B-283" "K-409" "B-409" "K-571" "B-571" "secp160k1" "secp160r1" "secp160r2" "secp192k1" "P-192" "secp224k1" "P-224" "secp256k1" "P-256" "P-384" "P-521" "brainpoolP256r1" "brainpoolP384r1" "brainpoolP512r1" "X25519" "X448")
+ local -ai curves_bits=(163 162 163 193 193 232 233 238 281 282 407 409 570 570 161 161 161 192 192 225 224 256 256 384 521 256 384 512 253 448)
+ # Many curves have been deprecated, and RFC 8446, Appendix B.3.1.4, states
+ # that these curves MUST NOT be offered in a TLS 1.3 ClientHello.
+ local -a curves_deprecated=("true" "true" "true" "true" "true" "true" "true" "true" "true" "true" "true" "true" "true" "true" "true" "true" "true" "true" "true" "true" "true" "true" "false" "false" "false" "true" "true" "true" "false" "false")
+ local -a ffdhe_groups_hex=("01,00" "01,01" "01,02" "01,03" "01,04")
+ local -a ffdhe_groups_output=("ffdhe2048" "ffdhe3072" "ffdhe4096" "ffdhe6144" "ffdhe8192")
+ local -a supported_curve
+ local -i nr_supported_ciphers=0 nr_curves=0 nr_ossl_curves=0 i j low high
+ local pfs_ciphers curves_offered="" curves_to_test temp
+ local len1 len2 curve_found
+ local key_bitstring quality_str
+ local -i len_dh_p quality
+ local has_dh_bits="$HAS_DH_BITS"
+ local using_sockets=true
+ local jsonID="PFS"
+
+ "$SSL_NATIVE" && using_sockets=false
+ "$FAST" && using_sockets=false
+ [[ $TLS_NR_CIPHERS == 0 ]] && using_sockets=false
+
+ outln
+ pr_headline " Testing robust (perfect) forward secrecy"; prln_underline ", (P)FS -- omitting Null Authentication/Encryption, 3DES, RC4 "
+ if ! "$using_sockets"; then
+ [[ $TLS_NR_CIPHERS == 0 ]] && ! "$SSL_NATIVE" && ! "$FAST" && pr_warning " Cipher mapping not available, doing a fallback to openssl"
+ if ! "$HAS_DH_BITS" && "$WIDE"; then
+ [[ $TLS_NR_CIPHERS == 0 ]] && ! "$SSL_NATIVE" && ! "$FAST" && out "."
+ pr_warning " (Your $OPENSSL cannot show DH/ECDH bits)"
+ fi
+ outln
+ fi
+
+ if "$using_sockets" || [[ $OSSL_VER_MAJOR -lt 1 ]]; then
+ for (( i=0; i < TLS_NR_CIPHERS; i++ )); do
+ pfs_cipher="${TLS_CIPHER_RFC_NAME[i]}"
+ hexc="${TLS_CIPHER_HEXCODE[i]}"
+ if ( [[ "$pfs_cipher" == "TLS_DHE_"* ]] || [[ "$pfs_cipher" == "TLS_ECDHE_"* ]] || [[ "${hexc:2:2}" == "13" ]] ) && \
+ [[ ! "$pfs_cipher" =~ NULL ]] && [[ ! "$pfs_cipher" =~ DES ]] && [[ ! "$pfs_cipher" =~ RC4 ]] && \
+ [[ ! "$pfs_cipher" =~ PSK ]] && ( "$using_sockets" || "${TLS_CIPHER_OSSL_SUPPORTED[i]}" ); then
+ pfs_hex_cipher_list+=", ${hexc:2:2},${hexc:7:2}"
+ ciph[nr_supported_ciphers]="${TLS_CIPHER_OSSL_NAME[i]}"
+ rfc_ciph[nr_supported_ciphers]="${TLS_CIPHER_RFC_NAME[i]}"
+ kx[nr_supported_ciphers]="${TLS_CIPHER_KX[i]}"
+ enc[nr_supported_ciphers]="${TLS_CIPHER_ENC[i]}"
+ ciphers_found[nr_supported_ciphers]=false
+ sigalg[nr_supported_ciphers]=""
+ ossl_supported[nr_supported_ciphers]="${TLS_CIPHER_OSSL_SUPPORTED[i]}"
+ hexcode[nr_supported_ciphers]="${hexc:2:2},${hexc:7:2}"
+ if [[ "${hexc:2:2}" == 00 ]]; then
+ normalized_hexcode[nr_supported_ciphers]="x${hexc:7:2}"
+ else
+ normalized_hexcode[nr_supported_ciphers]="x${hexc:2:2}${hexc:7:2}"
+ fi
+ "$using_sockets" && ! "$has_dh_bits" && "$WIDE" && ossl_supported[nr_supported_ciphers]=false
+ nr_supported_ciphers+=1
+ fi
+ done
+ else
+ while read -r hexc dash ciph[nr_supported_ciphers] sslvers kx[nr_supported_ciphers] auth enc[nr_supported_ciphers] mac export; do
+ ciphers_found[nr_supported_ciphers]=false
+ if [[ "${hexc:2:2}" == 00 ]]; then
+ normalized_hexcode[nr_supported_ciphers]="x${hexc:7:2}"
+ else
+ normalized_hexcode[nr_supported_ciphers]="x${hexc:2:2}${hexc:7:2}"
+ fi
+ sigalg[nr_supported_ciphers]=""
+ ossl_supported[nr_supported_ciphers]=true
+ nr_supported_ciphers+=1
+ done < <(actually_supported_osslciphers "$pfs_cipher_list" "ALL" "-V")
+ fi
+ export=""
+
+ if [[ $(has_server_protocol "tls1_3") -eq 0 ]]; then
+ # All TLSv1.3 cipher suites offer robust PFS.
+ sclient_success=0
+ elif "$using_sockets"; then
+ tls_sockets "04" "${pfs_hex_cipher_list:2}, 00,ff"
+ sclient_success=$?
+ [[ $sclient_success -eq 2 ]] && sclient_success=0
+ else
+ debugme echo $nr_supported_ciphers
+ debugme echo $(actually_supported_osslciphers $pfs_cipher_list "ALL")
+ if [[ "$nr_supported_ciphers" -le "$CLIENT_MIN_PFS" ]]; then
+ outln
+ prln_local_problem "You only have $nr_supported_ciphers PFS ciphers on the client side "
+ fileout "$jsonID" "WARN" "tests skipped as you only have $nr_supported_ciphers PFS ciphers on the client site. ($CLIENT_MIN_PFS are required)"
+ return 1
+ fi
+ $OPENSSL s_client $(s_client_options "-cipher $pfs_cipher_list -ciphersuites "ALL" $STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY $SNI") >$TMPFILE 2>$ERRFILE </dev/null
+ sclient_connect_successful $? $TMPFILE
+ sclient_success=$?
+ [[ $sclient_success -eq 0 ]] && [[ $(grep -ac "BEGIN CERTIFICATE" $TMPFILE) -eq 0 ]] && sclient_success=1
+ fi
+
+ if [[ $sclient_success -ne 0 ]]; then
+ outln
+ prln_svrty_medium " No ciphers supporting Forward Secrecy offered"
+ fileout "$jsonID" "MEDIUM" "No ciphers supporting (P)FS offered"
+ else
+ outln
+ pfs_offered=true
+ pfs_ciphers=""
+ pr_svrty_good " PFS is offered (OK)"
+ fileout "$jsonID" "OK" "offered"
+ if "$WIDE"; then
+ outln ", ciphers follow (client/browser support is important here) \n"
+ neat_header
+ else
+ out " "
+ fi
+ if "$HAS_TLS13"; then
+ protos_to_try="-no_ssl2 -no_tls1_3"
+ else
+ protos_to_try="-no_ssl2"
+ fi
+
+ for proto in $protos_to_try; do
+ while true; do
+ ciphers_to_test=""
+ tls13_ciphers_to_test=""
+ for (( i=0; i < nr_supported_ciphers; i++ )); do
+ if ! "${ciphers_found[i]}" && "${ossl_supported[i]}"; then
+ if [[ "${ciph[i]}" == TLS13* ]] || [[ "${ciph[i]}" == TLS_* ]]; then
+ tls13_ciphers_to_test+=":${ciph[i]}"
+ else
+ ciphers_to_test+=":${ciph[i]}"
+ fi
+ fi
+ done
+ [[ -z "$ciphers_to_test" ]] && [[ -z "$tls13_ciphers_to_test" ]] && break
+ $OPENSSL s_client $(s_client_options "$proto -cipher "\'${ciphers_to_test:1}\'" -ciphersuites "\'${tls13_ciphers_to_test:1}\'" $STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY $SNI") &>$TMPFILE </dev/null
+ sclient_connect_successful $? $TMPFILE || break
+ pfs_cipher=$(get_cipher $TMPFILE)
+ [[ -z "$pfs_cipher" ]] && break
+ for (( i=0; i < nr_supported_ciphers; i++ )); do
+ [[ "$pfs_cipher" == "${ciph[i]}" ]] && break
+ done
+ [[ $i -eq $nr_supported_ciphers ]] && break
+ ciphers_found[i]=true
+ if [[ "$pfs_cipher" == TLS13* ]] || [[ "$pfs_cipher" == TLS_* ]]; then
+ pfs_tls13_offered=true
+ "$WIDE" && kx[i]="$(read_dhtype_from_file $TMPFILE)"
+ fi
+ if "$WIDE"; then
+ dhlen=$(read_dhbits_from_file "$TMPFILE" quiet)
+ kx[i]="${kx[i]} $dhlen"
+ fi
+ "$WIDE" && "$SHOW_SIGALGO" && grep -q "\-\-\-\-\-BEGIN CERTIFICATE\-\-\-\-\-" $TMPFILE && \
+ sigalg[i]="$(read_sigalg_from_file "$TMPFILE")"
+ done
+ done
+ if "$using_sockets"; then
+ for proto in 04 03; do
+ while true; do
+ ciphers_to_test=""
+ for (( i=0; i < nr_supported_ciphers; i++ )); do
+ ! "${ciphers_found[i]}" && ciphers_to_test+=", ${hexcode[i]}"
+ done
+ [[ -z "$ciphers_to_test" ]] && break
+ [[ "$proto" == "04" ]] && [[ ! "$ciphers_to_test" =~ ,\ 13,[0-9a-f][0-9a-f] ]] && break
+ ciphers_to_test="$(strip_inconsistent_ciphers "$proto" "$ciphers_to_test")"
+ [[ -z "$ciphers_to_test" ]] && break
+ if "$WIDE" && "$SHOW_SIGALGO"; then
+ tls_sockets "$proto" "${ciphers_to_test:2}, 00,ff" "all"
+ else
+ tls_sockets "$proto" "${ciphers_to_test:2}, 00,ff" "ephemeralkey"
+ fi
+ sclient_success=$?
+ [[ $sclient_success -ne 0 ]] && [[ $sclient_success -ne 2 ]] && break
+ pfs_cipher=$(get_cipher "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt")
+ for (( i=0; i < nr_supported_ciphers; i++ )); do
+ [[ "$pfs_cipher" == "${rfc_ciph[i]}" ]] && break
+ done
+ [[ $i -eq $nr_supported_ciphers ]] && break
+ ciphers_found[i]=true
+ if [[ "${kx[i]}" == Kx=any ]]; then
+ pfs_tls13_offered=true
+ "$WIDE" && kx[i]="$(read_dhtype_from_file "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt")"
+ fi
+ if "$WIDE"; then
+ dhlen=$(read_dhbits_from_file "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt" quiet)
+ kx[i]="${kx[i]} $dhlen"
+ fi
+ "$WIDE" && "$SHOW_SIGALGO" && [[ -r "$HOSTCERT" ]] && \
+ sigalg[i]="$(read_sigalg_from_file "$HOSTCERT")"
+ done
+ done
+ fi
+ for (( i=0; i < nr_supported_ciphers; i++ )); do
+ ! "${ciphers_found[i]}" && ! "$SHOW_EACH_C" && continue
+ if "${ciphers_found[i]}"; then
+ if ( [[ "$DISPLAY_CIPHERNAMES" =~ openssl ]] && [[ "${ciph[i]}" != "-" ]] ) || [[ "${rfc_ciph[i]}" == "-" ]]; then
+ pfs_cipher="${ciph[i]}"
+ else
+ pfs_cipher="${rfc_ciph[i]}"
+ fi
+ pfs_ciphers+="$pfs_cipher "
+
+ if [[ "${ciph[i]}" == ECDHE-* ]] || [[ "${ciph[i]}" == TLS13* ]] || [[ "${ciph[i]}" == TLS_* ]] || ( "$using_sockets" && [[ "${rfc_ciph[i]}" == TLS_ECDHE_* ]] ); then
+ ecdhe_offered=true
+ ecdhe_cipher_list_hex+=", ${hexcode[i]}"
+ if [[ "${ciph[i]}" != "-" ]]; then
+ if [[ "${ciph[i]}" == TLS13* ]] || [[ "${ciph[i]}" == TLS_* ]]; then
+ tls13_cipher_list+=":$pfs_cipher"
+ else
+ ecdhe_cipher_list+=":$pfs_cipher"
+ fi
+ fi
+ fi
+ if [[ "${ciph[i]}" == "DHE-"* ]] || ( "$using_sockets" && [[ "${rfc_ciph[i]}" == "TLS_DHE_"* ]] ); then
+ ffdhe_offered=true
+ ffdhe_cipher_list_hex+=", ${hexcode[i]}"
+ elif [[ "${ciph[i]}" == TLS13* ]] || [[ "${ciph[i]}" == TLS_* ]]; then
+ ffdhe_cipher_list_hex+=", ${hexcode[i]}"
+ fi
+ fi
+ if "$WIDE"; then
+ neat_list "$(tolower "${normalized_hexcode[i]}")" "${ciph[i]}" "${kx[i]}" "${enc[i]}" "${ciphers_found[i]}"
+ if "$SHOW_EACH_C"; then
+ if "${ciphers_found[i]}"; then
+ pr_cipher_quality "${rfc_ciph[i]}" "available"
+ else
+ pr_deemphasize "not a/v"
+ fi
+ fi
+ outln "${sigalg[i]}"
+ fi
+ done
+ if ! "$WIDE"; then
+ if [[ "$COLOR" -le 2 ]]; then
+ out "$(out_row_aligned_max_width "$pfs_ciphers" " " $TERM_WIDTH)"
+ else
+ out_row_aligned_max_width_by_entry "$pfs_ciphers" " " $TERM_WIDTH pr_cipher_quality
+ fi
+ fi
+ debugme echo $pfs_offered
+ "$WIDE" || outln
+ fileout "${jsonID}_ciphers" "INFO" "$pfs_ciphers"
+ fi
+
+ # find out what elliptic curves are supported.
+ if "$ecdhe_offered"; then
+ for curve in "${curves_ossl[@]}"; do
+ ossl_supported[nr_curves]=false
+ supported_curve[nr_curves]=false
+ [[ "$OSSL_SUPPORTED_CURVES" =~ " $curve " ]] && ossl_supported[nr_curves]=true && nr_ossl_curves+=1
+ nr_curves+=1
+ done
+
+ # OpenSSL limits the number of curves that can be specified in the
+ # "-curves" option to 28. So, break the list in two if there are more
+ # than 28 curves supported by OpenSSL.
+ for j in 1 2; do
+ if [[ $j -eq 1 ]]; then
+ if [[ $nr_ossl_curves -le 28 ]]; then
+ low=0; high=$nr_curves
+ else
+ low=0; high=$nr_curves/2
+ fi
+ else
+ if [[ $nr_ossl_curves -le 28 ]]; then
+ continue # all curves tested in first round
+ else
+ low=$nr_curves/2; high=$nr_curves
+ fi
+ fi
+ if "$HAS_TLS13"; then
+ if "$pfs_tls13_offered"; then
+ protos_to_try="-no_ssl2 -no_tls1_3"
+ else
+ protos_to_try="-no_tls1_3"
+ fi
+ else
+ protos_to_try="-no_ssl2"
+ fi
+
+ for proto in $protos_to_try; do
+ while true; do
+ curves_to_test=""
+ for (( i=low; i < high; i++ )); do
+ if ! "$HAS_TLS13" || ! "${curves_deprecated[i]}" || [[ "$proto" == "-no_tls1_3" ]]; then
+ "${ossl_supported[i]}" && ! "${supported_curve[i]}" && curves_to_test+=":${curves_ossl[i]}"
+ fi
+ done
+ [[ -z "$curves_to_test" ]] && break
+ $OPENSSL s_client $(s_client_options "$proto -cipher "\'${ecdhe_cipher_list:1}\'" -ciphersuites "\'${tls13_cipher_list:1}\'" -curves "${curves_to_test:1}" $STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY $SNI") &>$TMPFILE </dev/null
+ sclient_connect_successful $? $TMPFILE || break
+ temp=$(awk -F': ' '/^Server Temp Key/ { print $2 }' "$TMPFILE")
+ curve_found="${temp%%,*}"
+ if [[ "$curve_found" == ECDH ]]; then
+ curve_found="${temp#*, }"
+ curve_found="${curve_found%%,*}"
+ fi
+ for (( i=low; i < high; i++ )); do
+ ! "${supported_curve[i]}" && [[ "${curves_ossl_output[i]}" == "$curve_found" ]] && break
+ done
+ [[ $i -eq $high ]] && break
+ supported_curve[i]=true
+ done
+ done
+ done
+ fi
+ if "$ecdhe_offered" && "$using_sockets"; then
+ protos_to_try="03"
+ "$pfs_tls13_offered" && protos_to_try="04 03"
+ for proto in $protos_to_try; do
+ if [[ "$proto" == 03 ]]; then
+ ecdhe_cipher_list_hex="$(strip_inconsistent_ciphers "03" "$ecdhe_cipher_list_hex")"
+ [[ -z "$ecdhe_cipher_list_hex" ]] && continue
+ fi
+ while true; do
+ curves_to_test=""
+ for (( i=0; i < nr_curves; i++ )); do
+ if ! "${curves_deprecated[i]}" || [[ "$proto" == 03 ]]; then
+ ! "${supported_curve[i]}" && curves_to_test+=", ${curves_hex[i]}"
+ fi
+ done
+ [[ -z "$curves_to_test" ]] && break
+ len1=$(printf "%02x" "$((2*${#curves_to_test}/7))")
+ len2=$(printf "%02x" "$((2*${#curves_to_test}/7+2))")
+ tls_sockets "$proto" "${ecdhe_cipher_list_hex:2}, 00,ff" "ephemeralkey" "00, 0a, 00, $len2, 00, $len1, ${curves_to_test:2}"
+ sclient_success=$?
+ [[ $sclient_success -ne 0 ]] && [[ $sclient_success -ne 2 ]] && break
+ temp=$(awk -F': ' '/^Server Temp Key/ { print $2 }' "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt")
+ curve_found="${temp%%,*}"
+ if [[ "$curve_found" == "ECDH" ]]; then
+ curve_found="${temp#*, }"
+ curve_found="${curve_found%%,*}"
+ fi
+ for (( i=0; i < nr_curves; i++ )); do
+ ! "${supported_curve[i]}" && [[ "${curves_ossl_output[i]}" == "$curve_found" ]] && break
+ done
+ [[ $i -eq $nr_curves ]] && break
+ supported_curve[i]=true
+ done
+ done
+ fi
+ if "$ecdhe_offered"; then
+ low=1000
+ for (( i=0; i < nr_curves; i++ )); do
+ if "${supported_curve[i]}"; then
+ curves_offered+="${curves_ossl[i]} "
+ [[ ${curves_bits[i]} -lt $low ]] && low=${curves_bits[i]}
+ fi
+ done
+ if [[ -n "$curves_offered" ]]; then
+ "$WIDE" && outln
+ pr_bold " Elliptic curves offered: "
+ out_row_aligned_max_width_by_entry "$curves_offered" " " $TERM_WIDTH pr_ecdh_curve_quality
+ outln
+ # severity ratings based on quality specified by
+ # pr_ecdh_quality() for shortest curve offered.
+ if [[ "$low" -le 163 ]]; then
+ fileout "${jsonID}_ECDHE_curves" "MEDIUM" "$curves_offered"
+ elif [[ "$low" -le 193 ]]; then
+ fileout "${jsonID}_ECDHE_curves" "LOW" "$curves_offered"
+ elif [[ "$low" -le 224 ]]; then
+ fileout "${jsonID}_ECDHE_curves" "INFO" "$curves_offered"
+ else
+ fileout "${jsonID}_ECDHE_curves" "OK" "$curves_offered"
+ fi
+ fi
+ fi
+ CURVES_OFFERED="$curves_offered"
+ CURVES_OFFERED=$(strip_trailing_space "$CURVES_OFFERED")
+
+ # find out what groups are supported.
+ if "$using_sockets" && ( "$pfs_tls13_offered" || "$ffdhe_offered" ); then
+ nr_curves=0
+ for curve in "${ffdhe_groups_output[@]}"; do
+ supported_curve[nr_curves]=false
+ [[ "$DH_GROUP_OFFERED" =~ $curve ]] && supported_curve[nr_curves]=true
+ nr_curves+=1
+ done
+ protos_to_try=""
+ "$pfs_tls13_offered" && protos_to_try="04"
+ if "$ffdhe_offered"; then
+ if "$pfs_tls13_offered"; then
+ protos_to_try="04 03"
+ else
+ protos_to_try="03"
+ fi
+ fi
+ curve_found=""
+ for proto in $protos_to_try; do
+ while true; do
+ curves_to_test=""
+ for (( i=0; i < nr_curves; i++ )); do
+ ! "${supported_curve[i]}" && curves_to_test+=", ${ffdhe_groups_hex[i]}"
+ done
+ [[ -z "$curves_to_test" ]] && break
+ len1=$(printf "%02x" "$((2*${#curves_to_test}/7))")
+ len2=$(printf "%02x" "$((2*${#curves_to_test}/7+2))")
+ tls_sockets "$proto" "${ffdhe_cipher_list_hex:2}, 00,ff" "ephemeralkey" "00, 0a, 00, $len2, 00, $len1, ${curves_to_test:2}"
+ sclient_success=$?
+ [[ $sclient_success -ne 0 ]] && [[ $sclient_success -ne 2 ]] && break
+ temp=$(awk -F': ' '/^Server Temp Key/ { print $2 }' "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt")
+ curve_found="${temp#*, }"
+ curve_found="${curve_found%%,*}"
+ if [[ "$proto" == "03" ]] && [[ -z "$DH_GROUP_OFFERED" ]] && [[ "$curve_found" =~ ffdhe ]]; then
+ DH_GROUP_OFFERED="RFC7919/$curve_found"
+ DH_GROUP_LEN_P="${curve_found#ffdhe}"
+ fi
+ [[ ! "$curve_found" =~ ffdhe ]] && break
+ for (( i=0; i < nr_curves; i++ )); do
+ ! "${supported_curve[i]}" && [[ "${ffdhe_groups_output[i]}" == "$curve_found" ]] && break
+ done
+ [[ $i -eq $nr_curves ]] && break
+ supported_curve[i]=true
+ done
+ done
+ curves_offered=""
+ for (( i=0; i < nr_curves; i++ )); do
+ "${supported_curve[i]}" && curves_offered+="${ffdhe_groups_output[i]} "
+ done
+ curves_offered="$(strip_trailing_space "$curves_offered")"
+ if "$ffdhe_offered" && [[ -z "$curves_offered" ]] && [[ -z "$curve_found" ]]; then
+ # Some servers will fail if the supported_groups extension is present.
+ tls_sockets "03" "${ffdhe_cipher_list_hex:2}, 00,ff" "ephemeralkey"
+ sclient_success=$?
+ if [[ $sclient_success -eq 0 ]] || [[ $sclient_success -eq 2 ]]; then
+ temp=$(awk -F': ' '/^Server Temp Key/ { print $2 }' "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt")
+ curve_found="${temp#*, }"
+ curve_found="${curve_found%%,*}"
+ fi
+ fi
+ if [[ -z "$curves_offered" ]] && [[ -n "$curve_found" ]]; then
+ # The server is not using one of the groups from RFC 7919.
+ if [[ -z "$DH_GROUP_OFFERED" ]]; then
+ # this global will get the name of the group either here or in run_logjam()
+ key_bitstring="$(awk '/-----BEGIN PUBLIC KEY/,/-----END PUBLIC KEY/ { print $0 }' $TEMPDIR/$NODEIP.parse_tls_serverhello.txt)"
+ get_common_prime "$jsonID" "$key_bitstring" ""
+ case $? in
+ 0) curves_offered="$DH_GROUP_OFFERED"
+ len_dh_p=$DH_GROUP_LEN_P ;;
+ 2) pr_bold " DH or FF group offered : "
+ prln_local_problem "Your $OPENSSL does not support the pkey utility."
+ fileout "$jsonID" "WARN" "$OPENSSL does not support the pkey utility."
+ esac
+ else
+ curves_offered="$DH_GROUP_OFFERED"
+ len_dh_p=$DH_GROUP_LEN_P
+ fi
+ fi
+ if [[ -n "$curves_offered" ]]; then
+ if [[ ! "$curves_offered" =~ ffdhe ]] || [[ ! "$curves_offered" =~ \ ]]; then
+ pr_bold " DH group offered: "
+ else
+ pr_bold " Finite field group: "
+ fi
+ if [[ "$curves_offered" =~ ffdhe ]]; then
+ # ok not to display them in italics:
+ pr_svrty_good "$curves_offered"
+ quality=6
+ else
+ pr_dh "$curves_offered" "$len_dh_p"
+ quality=$?
+ fi
+ case "$quality" in
+ 1) quality_str="CRITICAL" ;;
+ 2) quality_str="HIGH" ;;
+ 3) quality_str="MEDIUM" ;;
+ 4) quality_str="LOW" ;;
+ 5) quality_str="INFO" ;;
+ 6|7) quality_str="OK" ;;
+ esac
+ if [[ "$curves_offered" =~ Unknown ]]; then
+ fileout "DH_groups" "$quality_str" "$curves_offered ($len_dh_p bits)"
+ else
+ fileout "DH_groups" "$quality_str" "$curves_offered"
+ fi
+ fi
+ fi
+ outln
+
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ "$using_sockets" && HAS_DH_BITS="$has_dh_bits"
+ return 0
+}
+
+
+# good source for configuration and bugs: https://wiki.mozilla.org/Security/Server_Side_TLS
+# good start to read: https://en.wikipedia.org/wiki/Transport_Layer_Security#Attacks_against_TLS.2FSSL
+
+
+npn_pre(){
+ if [[ -n "$PROXY" ]]; then
+ pr_warning "not tested as proxies do not support proxying it"
+ fileout "NPN" "WARN" "not tested as proxies do not support proxying it"
+ return 1
+ fi
+ if ! "$HAS_NPN"; then
+ pr_local_problem "$OPENSSL doesn't support NPN/SPDY";
+ fileout "NPN" "WARN" "not tested $OPENSSL doesn't support NPN/SPDY"
+ return 7
+ fi
+ return 0
+}
+
+alpn_pre(){
+ if [[ -n "$PROXY" ]]; then
+ pr_warning "not tested as proxies do not support proxying it"
+ fileout "ALPN" "WARN" "not tested as proxies do not support proxying it"
+ return 1
+ fi
+ if ! "$HAS_ALPN" && "$SSL_NATIVE"; then
+ prln_local_problem "$OPENSSL doesn't support ALPN/HTTP2";
+ fileout "ALPN" "WARN" "not tested as $OPENSSL does not support it"
+ return 7
+ fi
+ return 0
+}
+
+# modern browsers do not support it anymore but we should still test it at least for fingerprinting the server side
+# Thus we don't label any support for NPN as good.
+# FAST mode skips this test
+run_npn() {
+ local tmpstr
+ local -i ret=0
+ local jsonID="NPN"
+
+ [[ -n "$STARTTLS" ]] && return 0
+ "$FAST" && return 0
+ pr_bold " NPN/SPDY "
+ if ! npn_pre; then
+ outln
+ return 0
+ fi
+ $OPENSSL s_client $(s_client_options "-connect $NODEIP:$PORT $BUGS $SNI -nextprotoneg "$NPN_PROTOs"") </dev/null 2>$ERRFILE >$TMPFILE
+ [[ $? -ne 0 ]] && ret=1
+ tmpstr="$(grep -a '^Protocols' $TMPFILE | sed 's/Protocols.*: //')"
+ if [[ -z "$tmpstr" ]] || [[ "$tmpstr" == " " ]]; then
+ outln "not offered"
+ fileout "$jsonID" "INFO" "not offered"
+ else
+ # now comes a strange thing: "Protocols advertised by server:" is empty but connection succeeded
+ if [[ "$tmpstr" =~ [h2|spdy|http] ]]; then
+ out "$tmpstr"
+ outln " (advertised)"
+ fileout "$jsonID" "INFO" "offered with $tmpstr (advertised)"
+ else
+ prln_cyan "please check manually, server response was ambiguous ..."
+ fileout "$jsonID" "INFO" "please check manually, server response was ambiguous ..."
+ ((ret++))
+ fi
+ fi
+ # btw: nmap can do that too https://nmap.org/nsedoc/scripts/tls-nextprotoneg.html
+ # nmap --script=tls-nextprotoneg #NODE -p $PORT is your friend if your openssl doesn't want to test this
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return $ret
+}
+
+
+run_alpn() {
+ local tmpstr alpn_extn len
+ local -i ret=0
+ local has_alpn_proto=false
+ local alpn_finding=""
+ local jsonID="ALPN"
+
+ [[ -n "$STARTTLS" ]] && return 0
+ pr_bold " ALPN/HTTP2 "
+ if ! alpn_pre; then
+ outln
+ return 0
+ fi
+ for proto in $ALPN_PROTOs; do
+ # for some reason OpenSSL doesn't list the advertised protocols, so instead try common protocols
+ if "$HAS_ALPN"; then
+ $OPENSSL s_client $(s_client_options "-connect $NODEIP:$PORT $BUGS $SNI -alpn $proto") </dev/null 2>$ERRFILE >$TMPFILE
+ else
+ alpn_extn="$(printf "%02x" ${#proto}),$(string_to_asciihex "$proto")"
+ len="$(printf "%04x" $((${#proto}+1)))"
+ alpn_extn="${len:0:2},${len:2:2},$alpn_extn"
+ len="$(printf "%04x" $((${#proto}+3)))"
+ alpn_extn="00,10,${len:0:2},${len:2:2},$alpn_extn"
+ tls_sockets "03" "$TLS12_CIPHER" "all+" "$alpn_extn"
+ if [[ -r "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt" ]]; then
+ cp "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt" $TMPFILE
+ else
+ echo "" > $TMPFILE
+ fi
+ fi
+ tmpstr="$(awk -F':' '/^ALPN protocol*:/ { print $2 }' $TMPFILE)"
+ if [[ "$tmpstr" == *"$proto" ]]; then
+ if ! $has_alpn_proto; then
+ has_alpn_proto=true
+ else
+ out ", "
+ fi
+ # only h2 is what browser need to use HTTP/2.0 and brings a security,privacy and performance benefit
+ if [[ "$proto" == "h2" ]]; then
+ pr_svrty_good "$proto"
+ fileout "${jsonID}_HTTP2" "OK" "$proto"
+ else
+ out "$proto"
+ alpn_finding+="$proto"
+ fi
+ fi
+ done
+ if $has_alpn_proto; then
+ outln " (offered)"
+ # if h2 is not the only protocol:
+ [[ -n "$alpn_finding" ]] && fileout "$jsonID" "INFO" "$alpn_finding"
+ else
+ outln "not offered"
+ fileout "$jsonID" "INFO" "not offered"
+ fi
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return $ret
+}
+
+# arg1: send string
+# arg2: success string: an egrep pattern
+# arg3: number of loops we should read from the buffer (optional, otherwise STARTTLS_SLEEP)
+starttls_io() {
+ local nr_waits=$STARTTLS_SLEEP
+ local buffer=""
+ local -i i
+
+ [[ -n "$3" ]] && waitsleep=$3
+ [[ -z "$2" ]] && echo "FIXME $((LINENO))"
+
+ # If there's a sending part it's IO. Postgres sends via socket and replies via
+ # strings "S". So there's no I part of IO ;-)
+ if [[ -n "$1" ]]; then
+ debugme echo -en "C: $1"
+ echo -en "$1" >&5
+ fi
+
+ # This seems a bit dangerous but works. No blockings yet. "if=nonblock" doesn't work on BSDs
+ buffer="$(dd bs=512 count=1 <&5 2>/dev/null)"
+
+ for ((i=1; i < $nr_waits; i++ )); do
+ [[ "$DEBUG" -ge 2 ]] && echo -en "\nS: " && echo $buffer
+ if [[ "$buffer" =~ $2 ]]; then
+ debugme echo " ---> reply matched \"$2\""
+ # the fd sometimes still seem to contain chars which confuses the following TLS handshake, trying to empty:
+ # dd of=/dev/null bs=512 count=1 <&5 2>/dev/null
+ return 0
+ else
+ # no match yet, more reading from fd helps.
+ buffer+=$(dd bs=512 count=1 <&5 2>/dev/null)
+ fi
+ done
+ return 1
+}
+
+
+# Line-based send with newline characters appended (arg2 empty)
+# Stream-based send: arg2: <any>
+starttls_just_send(){
+ if [[ -z "$2" ]] ; then
+ debugme echo -e "C: $1 plus lf"
+ echo -ne "$1\r\n" >&5
+ else
+ debugme echo -e "C: $1"
+ echo -ne "$1" >&5
+ fi
+ return $?
+}
+
+# arg1: (optional): wait time
+starttls_just_read(){
+ local waitsleep=$STARTTLS_SLEEP
+ [[ -n "$1" ]] && waitsleep=$1
+ if [[ "$DEBUG" -ge 2 ]]; then
+ echo "=== just read banner ==="
+ cat <&5 &
+ else
+ dd of=/dev/null count=8 <&5 2>/dev/null &
+ fi
+ wait_kill $! $waitsleep
+ return 0
+}
+
+starttls_full_read(){
+ local starttls_read_data=()
+ local one_line=""
+ local ret=0
+ local cont_pattern="$1"
+ local end_pattern="$2"
+ local ret_found=0
+
+ debugme echo "=== reading banner ... ==="
+ if [[ $# -ge 3 ]]; then
+ debugme echo "=== we'll have to search for \"$3\" pattern ==="
+ ret_found=3
+ fi
+
+ local oldIFS="$IFS"
+ IFS=''
+ while read -r -t $STARTTLS_SLEEP one_line; ret=$?; (exit $ret); do
+ debugme echo "S: ${one_line}"
+ if [[ $# -ge 3 ]]; then
+ if [[ ${one_line} =~ $3 ]]; then
+ ret_found=0
+ debugme echo "^^^^^^^ that's what we were looking for ==="
+ fi
+ fi
+ starttls_read_data+=("${one_line}")
+ if [[ $DEBUG -ge 4 ]]; then
+ echo "one_line: ${one_line}"
+ echo "end_pattern: ${end_pattern}"
+ echo "cont_pattern: ${cont_pattern}"
+ fi
+ if [[ ${one_line} =~ ${end_pattern} ]]; then
+ debugme echo "=== full read finished ==="
+ IFS="${oldIFS}"
+ return ${ret_found}
+ fi
+ if [[ ! ${one_line} =~ ${cont_pattern} ]]; then
+ debugme echo "=== full read syntax error, expected regex pattern ${cont_pattern} (cont) or ${end_pattern} (end) ==="
+ IFS="${oldIFS}"
+ return 2
+ fi
+ done <&5
+ if [[ $DEBUG -ge 2 ]]; then
+ if [[ $ret -ge 128 ]]; then
+ echo "=== timeout reading ==="
+ else
+ echo "=== full read error (no timeout) ==="
+ fi
+ fi
+ IFS="${oldIFS}"
+ return $ret
+}
+
+starttls_ftp_dialog() {
+ debugme echo "=== starting ftp STARTTLS dialog ==="
+ local reAUTHTLS='^ AUTH TLS'
+ starttls_full_read '^220-' '^220 ' && debugme echo "received server greeting" &&
+ starttls_just_send 'FEAT' && debugme echo "sent FEAT" &&
+ starttls_full_read '^(211-| )' '^211 ' "${reAUTHTLS}" && debugme echo "received server features and checked STARTTLS availability" &&
+ starttls_just_send 'AUTH TLS' && debugme echo "initiated STARTTLS" &&
+ starttls_full_read '^234-' '^234 ' && debugme echo "received ack for STARTTLS"
+ local ret=$?
+ debugme echo "=== finished ftp STARTTLS dialog with ${ret} ==="
+ return $ret
+}
+
+# argv1: empty: SMTP, "lmtp" : LMTP
+#
+starttls_smtp_dialog() {
+ local greet_str="EHLO"
+ local proto="smtp"
+
+ if [[ "$1" == lmtp ]]; then
+ proto="lmtp"
+ greet_str="LHLO"
+ fi
+ debugme echo "=== starting $proto STARTTLS dialog ==="
+
+ local re250STARTTLS='^250[ -]STARTTLS'
+ starttls_full_read '^220-' '^220 ' && debugme echo "received server greeting" &&
+ starttls_just_send "$greet_str testssl.sh" && debugme echo "sent $greet_str" &&
+ starttls_full_read '^250-' '^250 ' "${re250STARTTLS}" && debugme echo "received server capabilities and checked STARTTLS availability" &&
+ starttls_just_send 'STARTTLS' && debugme echo "initiated STARTTLS" &&
+ starttls_full_read '^220-' '^220 ' && debugme echo "received ack for STARTTLS"
+ local ret=$?
+ debugme echo "=== finished $proto STARTTLS dialog with ${ret} ==="
+ return $ret
+}
+
+starttls_pop3_dialog() {
+ debugme echo "=== starting pop3 STARTTLS dialog ==="
+ starttls_full_read '^\+OK' '^\+OK' && debugme echo "received server greeting" &&
+ starttls_just_send 'STLS' && debugme echo "initiated STARTTLS" &&
+ starttls_full_read '^\+OK' '^\+OK' && debugme echo "received ack for STARTTLS"
+ local ret=$?
+ debugme echo "=== finished pop3 STARTTLS dialog with ${ret} ==="
+ return $ret
+}
+
+starttls_imap_dialog() {
+ debugme echo "=== starting imap STARTTLS dialog ==="
+ local reSTARTTLS='^\* CAPABILITY(( .*)? IMAP4rev1( .*)? STARTTLS(.*)?|( .*)? STARTTLS( .*)? IMAP4rev1(.*)?)$'
+ starttls_full_read '^\* ' '^\* OK ' && debugme echo "received server greeting" &&
+ starttls_just_send 'a001 CAPABILITY' && debugme echo "sent CAPABILITY" &&
+ starttls_full_read '^\* ' '^a001 OK ' "${reSTARTTLS}" && debugme echo "received server capabilities and checked STARTTLS availability" &&
+ starttls_just_send 'a002 STARTTLS' && debugme echo "initiated STARTTLS" &&
+ starttls_full_read '^\* ' '^a002 OK ' && debugme echo "received ack for STARTTLS"
+ local ret=$?
+ debugme echo "=== finished imap STARTTLS dialog with ${ret} ==="
+ return $ret
+}
+
+starttls_xmpp_dialog() {
+ debugme echo "=== starting xmpp STARTTLS dialog ==="
+ [[ -z $XMPP_HOST ]] && XMPP_HOST="$NODE"
+
+ starttls_io "<stream:stream xmlns:stream='http://etherx.jabber.org/streams' xmlns='jabber:client' to='"$XMPP_HOST"' version='1.0'>" 'starttls(.*)features' 1 &&
+ starttls_io "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>" '<proceed' 1
+ local ret=$?
+ debugme echo "=== finished xmpp STARTTLS dialog with ${ret} ==="
+ return $ret
+}
+
+starttls_nntp_dialog() {
+ debugme echo "=== starting nntp STARTTLS dialog ==="
+ starttls_full_read '$^' '^20[01] ' && debugme echo "received server greeting" &&
+ starttls_just_send 'STARTTLS' && debugme echo "initiated STARTTLS" &&
+ starttls_full_read '$^' '^382 ' && debugme echo "received ack for STARTTLS"
+ local ret=$?
+ debugme echo "=== finished nntp STARTTLS dialog with ${ret} ==="
+ return $ret
+}
+
+starttls_postgres_dialog() {
+ debugme echo "=== starting postgres STARTTLS dialog ==="
+ local init_tls=", x00, x00 ,x00 ,x08 ,x04 ,xD2 ,x16 ,x2F"
+ socksend "${init_tls}" 0 && debugme echo "initiated STARTTLS" &&
+ starttls_io "" S 1 && debugme echo "received ack (="S") for STARTTLS"
+ local ret=$?
+ debugme echo "=== finished postgres STARTTLS dialog with ${ret} ==="
+ return $ret
+}
+
+starttls_mysql_dialog() {
+ debugme echo "=== starting mysql STARTTLS dialog ==="
+ local login_request="
+ , x20, x00, x00, x01, # payload_length, sequence_id
+ x85, xae, xff, x00, # capability flags, CLIENT_SSL always set
+ x00, x00, x00, x01, # max-packet size
+ x21, # character set
+ x00, x00, x00, x00, x00, x00, x00, x00, # string[23] reserved (all [0])
+ x00, x00, x00, x00, x00, x00, x00, x00,
+ x00, x00, x00, x00, x00, x00, x00"
+ socksend "${login_request}" 0
+ starttls_just_read 1 && debugme echo "read succeeded"
+ # 1 is the timeout value which only MySQL needs. Note, there seems no response whether STARTTLS
+ # succeeded. We could try harder, see https://github.com/openssl/openssl/blob/master/apps/s_client.c
+ # but atm this seems sufficient as later we will fail if there's no STARTTLS.
+ # BUT: there seem to be cases when the handshake fails (8S01Bad handshake --> 30 38 53 30 31 42 61 64 20 68 61 6e 64 73 68 61 6b 65).
+ # also there's a banner in the reply "<version><somebytes>mysql_native_password"
+ # TODO: We could detect if the server supports STARTTLS via the "Server Capabilities"
+ # bit field, but we'd need to parse the binary stream, with greater precision than regex.
+ local ret=$?
+ debugme echo "=== finished mysql STARTTLS dialog with ${ret} ==="
+ return $ret
+}
+
+# arg1: fd for socket -- which we don't use as it is a hassle and it is not clear whether it works under every bash version
+# returns 6 if opening the socket caused a problem, 1 if STARTTLS handshake failed, 0: all ok
+#
+fd_socket() {
+ local jabber=""
+ local proyxline=""
+ local nodeip="$(tr -d '[]' <<< $NODEIP)" # sockets do not need the square brackets we have of IPv6 addresses
+ # we just need do it here, that's all!
+ if [[ -t 5 ]]; then
+ pr_warning "$PROG_NAME: unable to open a socket because of a tty conflict"
+ return 6
+ fi
+ if [[ -n "$PROXY" ]]; then
+ # PROXYNODE works better than PROXYIP on modern versions of squid
+ if ! exec 5<> /dev/tcp/${PROXYNODE}/${PROXYPORT}; then
+ outln
+ pr_warning "$PROG_NAME: unable to open a socket to proxy $PROXYNODE:$PROXYPORT"
+ return 6
+ fi
+ if "$DNS_VIA_PROXY"; then
+ printf -- "%b" "CONNECT $NODE:$PORT HTTP/1.0\n\n" >&5
+ else
+ printf -- "%b" "CONNECT $nodeip:$PORT HTTP/1.0\n\n" >&5
+ fi
+ while true; do
+ read -t $PROXY_WAIT -r proyxline <&5
+ if [[ $? -ge 128 ]]; then
+ pr_warning "Proxy timed out. Unable to CONNECT via proxy. "
+ close_socket
+ return 6
+ elif [[ "${proyxline%/*}" == HTTP ]]; then
+ proyxline=${proyxline#* }
+ if [[ "${proyxline%% *}" != 200 ]]; then
+ pr_warning "Unable to CONNECT via proxy. "
+ [[ "$PORT" != 443 ]] && prln_warning "Check whether your proxy supports port $PORT and the underlying protocol."
+ close_socket
+ return 6
+ fi
+ fi
+ if [[ "$proyxline" == $'\r' ]] || [[ -z "$proyxline" ]] ; then
+ break
+ fi
+ done
+ # For the following execs: 2>/dev/null would remove a potential error message, but disables debugging.
+ # First we check whether a socket connect timeout was specified
+ elif [[ -n "$CONNECT_TIMEOUT" ]]; then
+ if ! $TIMEOUT_CMD $CONNECT_TIMEOUT bash -c "exec 5<>/dev/tcp/$nodeip/$PORT"; then
+ ((NR_SOCKET_FAIL++))
+ connectivity_problem $NR_SOCKET_FAIL $MAX_SOCKET_FAIL "TCP connect problem" "repeated TCP connect problems (connect timeout), giving up"
+ outln
+ pr_warning "Unable to open a socket to $NODEIP:$PORT. "
+ return 6
+ fi
+ # Now comes the the usual case
+ elif ! exec 5<>/dev/tcp/$nodeip/$PORT; then
+ ((NR_SOCKET_FAIL++))
+ connectivity_problem $NR_SOCKET_FAIL $MAX_SOCKET_FAIL "TCP connect problem" "repeated TCP connect problems, giving up"
+ outln
+ pr_warning "Unable to open a socket to $NODEIP:$PORT. "
+ return 6
+ fi
+
+ if [[ -n "$STARTTLS" ]]; then
+ case "$STARTTLS_PROTOCOL" in # port
+ ftp|ftps) # https://tools.ietf.org/html/rfc4217, https://tools.ietf.org/html/rfc959
+ starttls_ftp_dialog
+ ;;
+ smtp|smtps) # SMTP, see https://tools.ietf.org/html/rfc{2033,3207,5321}
+ starttls_smtp_dialog
+ ;;
+ lmtp|lmtps) # LMTP, see https://tools.ietf.org/html/rfc{2033,3207,5321}
+ starttls_smtp_dialog lmtp
+ ;;
+ pop3|pop3s) # POP, see https://tools.ietf.org/html/rfc2595
+ starttls_pop3_dialog
+ ;;
+ nntp|nntps) # NNTP, see https://tools.ietf.org/html/rfc4642
+ starttls_nntp_dialog
+ ;;
+ imap|imaps) # IMAP, https://tools.ietf.org/html/rfc2595, https://tools.ietf.org/html/rfc3501
+ starttls_imap_dialog
+ ;;
+ irc|ircs) # IRC, https://ircv3.net/specs/extensions/tls-3.1.html, https://ircv3.net/specs/core/capability-negotiation.html
+ fatal "FIXME: IRC+STARTTLS not yet supported" $ERR_NOSUPPORT
+ ;;
+ ldap|ldaps) # LDAP, https://tools.ietf.org/html/rfc2830, https://tools.ietf.org/html/rfc4511
+ fatal "FIXME: LDAP+STARTTLS over sockets not supported yet (try \"--ssl-native\")" $ERR_NOSUPPORT
+ ;;
+ acap|acaps) # ACAP = Application Configuration Access Protocol, see https://tools.ietf.org/html/rfc2595
+ fatal "ACAP Easteregg: not implemented -- probably never will" $ERR_NOSUPPORT
+ ;;
+ xmpp|xmpps) # XMPP, see https://tools.ietf.org/html/rfc6120
+ starttls_xmpp_dialog
+ # IM observatory: https://xmpp.net , XMPP server directory: https://xmpp.net/directory.php
+ ;;
+ postgres) # Postgres SQL, see https://www.postgresql.org/docs/devel/static/protocol-message-formats.html
+ starttls_postgres_dialog
+ ;;
+ mysql) # MySQL, see https://dev.mysql.com/doc/internals/en/x-protocol-lifecycle-lifecycle.html#x-protocol-lifecycle-tls-extension
+ starttls_mysql_dialog
+ ;;
+ *) # we need to throw an error here -- otherwise testssl.sh treats the STARTTLS protocol as plain SSL/TLS which leads to FP
+ fatal "FIXME: STARTTLS protocol $STARTTLS_PROTOCOL is not yet supported" $ERR_NOSUPPORT
+ esac
+ fi
+ [[ $? -eq 0 ]] && return 0
+ prln_warning " STARTTLS handshake failed"
+ return 1
+}
+
+close_socket(){
+ exec 5<&-
+ exec 5>&-
+ return 0
+}
+
+send_close_notify() {
+ local detected_tlsversion="$1"
+
+ debugme echo "sending close_notify..."
+ if [[ $detected_tlsversion == 0300 ]]; then
+ socksend ",x15, x03, x00, x00, x02, x02, x00" 0
+ else
+ socksend ",x15, x03, x01, x00, x02, x02, x00" 0
+ fi
+}
+
+# Format string properly for socket
+# ARG1: any commented sequence of two bytes hex, separated by commas. It can contain comments, new lines, tabs and white spaces
+# NW_STR holds the global with the string prepared for printf, like '\x16\x03\x03\'
+code2network() {
+ NW_STR=$(sed -e 's/,/\\\x/g' <<< "$1" | sed -e 's/# .*$//g' -e 's/ //g' -e '/^$/d' | tr -d '\n' | tr -d '\t')
+}
+
+# sockets inspired by https://blog.chris007.de/using-bash-for-network-socket-operation/
+# ARG1: hexbytes separated by commas, with a leading comma
+# ARG2: seconds to sleep
+socksend_clienthello() {
+ local data=""
+
+ code2network "$1"
+ data="$NW_STR"
+ [[ "$DEBUG" -ge 4 ]] && echo && echo "\"$data\""
+ if [[ -z "$PRINTF" ]] ;then
+ # We could also use "dd ibs=1M obs=1M" here but is seems to be at max 3% slower
+ printf -- "$data" | cat >&5 2>/dev/null &
+ else
+ $PRINTF -- "$data" 2>/dev/null >&5 2>/dev/null &
+ fi
+ sleep $USLEEP_SND
+}
+
+
+# ARG1: hexbytes -- preceded by x -- separated by commas, with a leading comma
+# ARG2: seconds to sleep
+socksend() {
+ local data line
+
+ # read line per line and strip comments (bash internal func can't handle multiline statements
+ data="$(while read line; do
+ printf "${line%%\#*}"
+ done <<< "$1" )"
+ data="${data// /}" # strip ' '
+ data="${data//,/\\}" # s&r , by \
+ [[ $DEBUG -ge 4 ]] && echo && echo "\"$data\""
+ if [[ -z "$PRINTF" ]] ;then
+ printf -- "$data" | cat >&5 2>/dev/null &
+ else
+ $PRINTF -- "$data" 2>/dev/null >&5 2>/dev/null &
+ fi
+ sleep $2
+}
+
+
+# for SSLv2 to TLS 1.2:
+# ARG1: blocksize for reading
+sockread_serverhello() {
+ [[ -z "$2" ]] && maxsleep=$MAX_WAITSOCK || maxsleep=$2
+ SOCK_REPLY_FILE=$(mktemp $TEMPDIR/ddreply.XXXXXX) || return 7
+ dd bs=$1 of=$SOCK_REPLY_FILE count=1 <&5 2>/dev/null &
+ wait_kill $! $maxsleep
+ return $?
+}
+
+#trying a faster version
+# ARG1: blocksize for reading
+sockread_fast() {
+ dd bs=$1 count=1 <&5 2>/dev/null | hexdump -v -e '16/1 "%02X"'
+}
+
+len2twobytes() {
+ local len_arg1=${#1}
+ [[ $len_arg1 -le 2 ]] && LEN_STR=$(printf "00, %02s \n" "$1")
+ [[ $len_arg1 -eq 3 ]] && LEN_STR=$(printf "0%s, %02s \n" "${1:0:1}" "${1:1:2}")
+ [[ $len_arg1 -eq 4 ]] && LEN_STR=$(printf "%02s, %02s \n" "${1:0:2}" "${1:2:2}")
+}
+
+
+get_pub_key_size() {
+ local pubkey pubkeybits
+ local -i i len1 len
+
+ "$HAS_PKEY" || return 1
+
+ # OpenSSL displays the number of bits for RSA and ECC
+ pubkeybits=$($OPENSSL x509 -noout -pubkey -in $HOSTCERT 2>>$ERRFILE | $OPENSSL pkey -pubin -text_pub 2>>$ERRFILE | awk -F'(' '/Public-Key/ { print $2 }')
+ if [[ -n $pubkeybits ]]; then
+ # remainder e.g. "256 bit)"
+ pubkeybits="${pubkeybits//\)/}"
+ echo "Server public key is $pubkeybits" >> $TMPFILE
+ else
+ # This extracts the public key for DSA, DH, and GOST
+ pubkey=$($OPENSSL x509 -noout -pubkey -in $HOSTCERT 2>>$ERRFILE | $OPENSSL pkey -pubin -outform DER 2>>$ERRFILE | hexdump -v -e '16/1 "%02X"')
+ [[ -z "$pubkey" ]] && return 1
+ # Skip over tag and length of subjectPublicKeyInfo
+ i=2
+ len1="0x${pubkey:i:2}"
+ if [[ $len1 -lt 0x80 ]]; then
+ i=$i+2
+ else
+ len1=$len1-0x80
+ i=$i+2*$len1+2
+ fi
+
+ # Skip over algorithm field
+ i=$i+2
+ len1="0x${pubkey:i:2}"
+ i=$i+2
+ if [[ $len1 -lt 0x80 ]]; then
+ i=$i+2*$len1
+ else
+ case $len1 in
+ 129) len="0x${pubkey:i:2}" ;;
+ 130) len="0x${pubkey:i:2}"
+ i=$i+2
+ len=256*$len+"0x${pubkey:i:2}"
+ ;;
+ 131) len="0x${pubkey:i:2}"
+ i=$i+2
+ len=256*$len+"0x${pubkey:i:2}"
+ i=$i+2
+ len=256*$len+"0x${pubkey:i:2}"
+ ;;
+ 132) len="0x${pubkey:i:2}"
+ i=$i+2
+ len=256*$len+"0x${pubkey:i:2}"
+ i=$i+2
+ len=256*$len+"0x${pubkey:i:2}"
+ i=$i+2
+ len=256*$len+"0x${pubkey:i:2}"
+ ;;
+ esac
+ i=$i+2+2*$len
+ fi
+
+ # Next is the public key BIT STRING. Skip over tag, length, and number of unused bits.
+ i=$i+2
+ len1="0x${pubkey:i:2}"
+ if [[ $len1 -lt 0x80 ]]; then
+ i=$i+4
+ else
+ len1=$len1-0x80
+ i=$i+2*$len1+4
+ fi
+
+ # Now get the length of the public key
+ i=$i+2
+ len1="0x${pubkey:i:2}"
+ i=$i+2
+ if [[ $len1 -lt 0x80 ]]; then
+ len=$len1
+ else
+ case $len1 in
+ 129) len="0x${pubkey:i:2}" ;;
+ 130) len="0x${pubkey:i:2}"
+ i=$i+2
+ len=256*$len+"0x${pubkey:i:2}"
+ ;;
+ 131) len="0x${pubkey:i:2}"
+ i=$i+2
+ len=256*$len+"0x${pubkey:i:2}"
+ i=$i+2
+ len=256*$len+"0x${pubkey:i:2}"
+ ;;
+ 132) len="0x${pubkey:i:2}"
+ i=$i+2
+ len=256*"0x${pubkey:i:2}"
+ i=$i+2
+ len=256*"0x${pubkey:i:2}"
+ i=$i+2
+ len=256*"0x${pubkey:i:2}"
+ ;;
+ esac
+ fi
+ len=8*$len # convert from bytes to bits
+ pubkeybits="$(printf "%d" $len)"
+ echo "Server public key is $pubkeybits bit" >> $TMPFILE
+ fi
+ return 0
+}
+
+# Extract the DH ephemeral key from the ServerKeyExchange message
+get_dh_ephemeralkey() {
+ local tls_serverkeyexchange_ascii="$1"
+ local -i tls_serverkeyexchange_ascii_len offset
+ local dh_p dh_g dh_y dh_param len1 key_bitstring
+ local -i i dh_p_len dh_g_len dh_y_len dh_param_len
+
+ "$HAS_PKEY" || return 1
+
+ tls_serverkeyexchange_ascii_len=${#tls_serverkeyexchange_ascii}
+ dh_p_len=2*$(hex2dec "${tls_serverkeyexchange_ascii:0:4}")
+ offset=4+$dh_p_len
+ if [[ $tls_serverkeyexchange_ascii_len -lt $offset ]]; then
+ debugme echo "Malformed ServerKeyExchange Handshake message in ServerHello."
+ return 1
+ fi
+
+ # Subtract any leading 0 bytes
+ for (( i=4; i < offset; i=i+2 )); do
+ [[ "${tls_serverkeyexchange_ascii:i:2}" != "00" ]] && break
+ dh_p_len=$dh_p_len-2
+ done
+ if [[ $i -ge $offset ]]; then
+ debugme echo "Malformed ServerKeyExchange Handshake message in ServerHello."
+ return 1
+ fi
+ dh_p="${tls_serverkeyexchange_ascii:i:dh_p_len}"
+
+ dh_g_len=2*$(hex2dec "${tls_serverkeyexchange_ascii:offset:4}")
+ i=4+$offset
+ offset+=4+$dh_g_len
+ if [[ $tls_serverkeyexchange_ascii_len -lt $offset ]]; then
+ debugme echo "Malformed ServerKeyExchange Handshake message in ServerHello."
+ return 1
+ fi
+ # Subtract any leading 0 bytes
+ for (( 1; i < offset; i=i+2 )); do
+ [[ "${tls_serverkeyexchange_ascii:i:2}" != "00" ]] && break
+ dh_g_len=$dh_g_len-2
+ done
+ if [[ $i -ge $offset ]]; then
+ debugme echo "Malformed ServerKeyExchange Handshake message in ServerHello."
+ return 1
+ fi
+ dh_g="${tls_serverkeyexchange_ascii:i:dh_g_len}"
+
+ dh_y_len=2*$(hex2dec "${tls_serverkeyexchange_ascii:offset:4}")
+ i=4+$offset
+ offset+=4+$dh_y_len
+ if [[ $tls_serverkeyexchange_ascii_len -lt $offset ]]; then
+ debugme echo "Malformed ServerKeyExchange Handshake message in ServerHello."
+ return 1
+ fi
+ # Subtract any leading 0 bytes
+ for (( 1; i < offset; i=i+2 )); do
+ [[ "${tls_serverkeyexchange_ascii:i:2}" != "00" ]] && break
+ dh_y_len=$dh_y_len-2
+ done
+ if [[ $i -ge $offset ]]; then
+ debugme echo "Malformed ServerKeyExchange Handshake message in ServerHello."
+ return 1
+ fi
+ dh_y="${tls_serverkeyexchange_ascii:i:dh_y_len}"
+
+ # The following code assumes that all lengths can be encoded using at most 2 bytes,
+ # which just means that the encoded length of the public key must be less than
+ # 65,536 bytes. If the length is anywhere close to that, it is almost certainly an
+ # encoding error.
+ if [[ $dh_p_len+$dh_g_len+$dh_y_len -ge 131000 ]]; then
+ debugme echo "Malformed ServerKeyExchange Handshake message in ServerHello."
+ return 1
+ fi
+ # make ASN.1 INTEGER of p, g, and Y
+ [[ "0x${dh_p:0:1}" -ge 8 ]] && dh_p_len+=2 && dh_p="00$dh_p"
+ if [[ $dh_p_len -lt 256 ]]; then
+ len1="$(printf "%02x" $((dh_p_len/2)))"
+ elif [[ $dh_p_len -lt 512 ]]; then
+ len1="81$(printf "%02x" $((dh_p_len/2)))"
+ else
+ len1="82$(printf "%04x" $((dh_p_len/2)))"
+ fi
+ dh_p="02${len1}$dh_p"
+
+ [[ "0x${dh_g:0:1}" -ge 8 ]] && dh_g_len+=2 && dh_g="00$dh_g"
+ if [[ $dh_g_len -lt 256 ]]; then
+ len1="$(printf "%02x" $((dh_g_len/2)))"
+ elif [[ $dh_g_len -lt 512 ]]; then
+ len1="81$(printf "%02x" $((dh_g_len/2)))"
+ else
+ len1="82$(printf "%04x" $((dh_g_len/2)))"
+ fi
+ dh_g="02${len1}$dh_g"
+
+ [[ "0x${dh_y:0:1}" -ge 8 ]] && dh_y_len+=2 && dh_y="00$dh_y"
+ if [[ $dh_y_len -lt 256 ]]; then
+ len1="$(printf "%02x" $((dh_y_len/2)))"
+ elif [[ $dh_y_len -lt 512 ]]; then
+ len1="81$(printf "%02x" $((dh_y_len/2)))"
+ else
+ len1="82$(printf "%04x" $((dh_y_len/2)))"
+ fi
+ dh_y="02${len1}$dh_y"
+
+ # Make a SEQUENCE of p and g
+ dh_param_len=${#dh_p}+${#dh_g}
+ if [[ $dh_param_len -lt 256 ]]; then
+ len1="$(printf "%02x" $((dh_param_len/2)))"
+ elif [[ $dh_param_len -lt 512 ]]; then
+ len1="81$(printf "%02x" $((dh_param_len/2)))"
+ else
+ len1="82$(printf "%04x" $((dh_param_len/2)))"
+ fi
+ dh_param="30${len1}${dh_p}${dh_g}"
+
+ # Make a SEQUENCE of the parameters SEQUENCE and the OID
+ dh_param_len=22+${#dh_param}
+ if [[ $dh_param_len -lt 256 ]]; then
+ len1="$(printf "%02x" $((dh_param_len/2)))"
+ elif [[ $dh_param_len -lt 512 ]]; then
+ len1="81$(printf "%02x" $((dh_param_len/2)))"
+ else
+ len1="82$(printf "%04x" $((dh_param_len/2)))"
+ fi
+ dh_param="30${len1}06092A864886F70D010301${dh_param}"
+
+ # Encapsulate public key, y, in a BIT STRING
+ dh_y_len=${#dh_y}+2
+ if [[ $dh_y_len -lt 256 ]]; then
+ len1="$(printf "%02x" $((dh_y_len/2)))"
+ elif [[ $dh_y_len -lt 512 ]]; then
+ len1="81$(printf "%02x" $((dh_y_len/2)))"
+ else
+ len1="82$(printf "%04x" $((dh_y_len/2)))"
+ fi
+ dh_y="03${len1}00$dh_y"
+
+ # Create the public key SEQUENCE
+ i=${#dh_param}+${#dh_y}
+ if [[ $i -lt 256 ]]; then
+ len1="$(printf "%02x" $((i/2)))"
+ elif [[ $i -lt 512 ]]; then
+ len1="81$(printf "%02x" $((i/2)))"
+ else
+ len1="82$(printf "%04x" $((i/2)))"
+ fi
+ key_bitstring="30${len1}${dh_param}${dh_y}"
+ key_bitstring="$(asciihex_to_binary "$key_bitstring" | $OPENSSL pkey -pubin -inform DER 2> $ERRFILE)"
+ [[ -z "$key_bitstring" ]] && return 1
+ tm_out "$key_bitstring"
+ return 0
+}
+
+# arg1: name of file with socket reply
+# arg2: true if entire server hello should be parsed
+# return values: 0=no SSLv2 (reset)
+# 1=no SSLv2 (plaintext reply like it happens with OLS webservers)
+# 3=SSLv2 supported (in $TEMPDIR/$NODEIP.sslv2_sockets.dd is reply for further processing
+# --> there could be checked whether ciphers e.g have been returned at all (or anything else)
+# 4=looks like an STARTTLS 5xx message
+# 6=socket couldn't be opened
+# 7=strange reply we can't deal with
+parse_sslv2_serverhello() {
+ local ret v2_hello_ascii v2_hello_initbyte v2_hello_length
+ local v2_hello_handshake v2_cert_type v2_hello_cert_length
+ local v2_hello_cipherspec_length
+ local -i certificate_len nr_ciphers_detected offset i
+ local ret=3
+ local parse_complete="false"
+ # SSLv2 server hello: in hex representation, see below
+ # byte 1+2: length of server hello 0123
+ # 3: 04=Handshake message, server hello 45
+ # 4: session id hit or not (boolean: 00=false, this 67
+ # is the normal case)
+ # 5: certificate type, 01 = x509 89
+ # 6+7 version (00 02 = SSLv2) 10-13
+ # 8+9 certificate length 14-17
+ # 10+11 cipher spec length 17-20
+ # 12+13 connection id length
+ # [certificate length] ==> certificate
+ # [cipher spec length] ==> ciphers GOOD: HERE ARE ALL CIPHERS ALREADY!
+
+ # Note: recent SSL/TLS stacks reply with a TLS alert on a SSLv2 client hello.
+ # The TLS error message is different and could be used for fingerprinting.
+
+ if [[ "$2" == "true" ]]; then
+ parse_complete=true
+ fi
+ "$parse_complete" && echo "======================================" > $TMPFILE
+
+ v2_hello_ascii=$(hexdump -v -e '16/1 "%02X"' $1)
+ v2_hello_ascii="${v2_hello_ascii%%[!0-9A-F]*}"
+ [[ "$DEBUG" -ge 5 ]] && echo "$v2_hello_ascii"
+ if [[ -z "$v2_hello_ascii" ]]; then
+ ret=0 # 1 line without any blanks: no server hello received
+ debugme echo "server hello empty"
+ else
+ # now scrape two bytes out of the reply per byte
+ v2_hello_initbyte="${v2_hello_ascii:0:1}" # normally this belongs to the next, should be 8!
+ v2_hello_length="${v2_hello_ascii:1:3}" # + 0x8000 see above
+ v2_hello_handshake="${v2_hello_ascii:4:2}"
+ v2_cert_type="${v2_hello_ascii:8:2}"
+ v2_hello_cert_length="${v2_hello_ascii:14:4}"
+ v2_hello_cipherspec_length="${v2_hello_ascii:18:4}"
+
+ V2_HELLO_CIPHERSPEC_LENGTH=$(printf "%d\n" "0x$v2_hello_cipherspec_length" 2>/dev/null)
+ [[ $? -ne 0 ]] && ret=7
+
+ if [[ "${v2_hello_ascii:0:2}" == "35" ]] && "$do_starttls"; then
+ # this could be a 500/5xx for some weird reason where the STARTTLS handshake failed
+ debugme echo "$(hex2ascii "$v2_hello_ascii")"
+ ret=4
+ elif [[ "${v2_hello_ascii:0:4}" == "1503" ]]; then
+ # Cloudflare does this, OpenSSL 1.1.1 and picoTLS. With different alert messages
+ # Just in case somebody's interested in the exact error, we deliver it ;-)
+ debugme echo -n ">TLS< alert message discovered: ${v2_hello_ascii} "
+ case "${v2_hello_ascii:10:2}" in
+ 01) debugme echo "(01/warning: 0x"${v2_hello_ascii:12:2}"/$(tls_alert "${v2_hello_ascii:12:2}"))" ;;
+ 02) debugme echo "(02/fatal: 0x"${v2_hello_ascii:12:2}"/$(tls_alert "${v2_hello_ascii:12:2}"))" ;;
+ *) debugme echo "("${v2_hello_ascii:10:2}" : "${v2_hello_ascii:12:2}"))" ;;
+ esac
+ ret=0
+ elif [[ $v2_hello_initbyte != "8" ]] || [[ $v2_hello_handshake != "04" ]]; then
+ ret=1
+ if [[ $DEBUG -ge 2 ]]; then
+ echo "no correct server hello"
+ echo "SSLv2 server init byte: 0x0$v2_hello_initbyte"
+ echo "SSLv2 hello handshake : 0x$v2_hello_handshake"
+ fi
+ fi
+
+ if [[ $DEBUG -ge 3 ]]; then
+ echo "SSLv2 server hello length: 0x0$v2_hello_length"
+ echo "SSLv2 certificate type: 0x$v2_cert_type"
+ echo "SSLv2 certificate length: 0x$v2_hello_cert_length"
+ echo "SSLv2 cipher spec length: 0x$v2_hello_cipherspec_length"
+ fi
+
+ if "$parse_complete" && [[ 2*$(hex2dec "$v2_hello_length") -ne ${#v2_hello_ascii}-4 ]]; then
+ ret=7
+ fi
+ fi
+
+ "$parse_complete" || return $ret
+
+ # not sure why we need this
+ rm -f $HOSTCERT
+ > $TEMPDIR/intermediatecerts.pem
+ if [[ $ret -eq 3 ]]; then
+ certificate_len=2*$(hex2dec "$v2_hello_cert_length")
+
+ if [[ "$v2_cert_type" == "01" ]] && [[ "$v2_hello_cert_length" != "00" ]]; then
+ asciihex_to_binary "${v2_hello_ascii:26:certificate_len}" | \
+ $OPENSSL x509 -inform DER -outform PEM -out $HOSTCERT 2>$ERRFILE
+ if [[ $? -ne 0 ]]; then
+ debugme echo "Malformed certificate in ServerHello."
+ return 1
+ fi
+ get_pub_key_size
+ echo "======================================" >> $TMPFILE
+ fi
+
+ # Output list of supported ciphers
+ offset=$((certificate_len+26))
+ nr_ciphers_detected=$((V2_HELLO_CIPHERSPEC_LENGTH / 3))
+ for (( i=0 ; i<nr_ciphers_detected; i++ )); do
+ echo "Supported cipher: x$(tolower "${v2_hello_ascii:offset:6}")" >> $TMPFILE
+ offset=$((offset+6))
+ done
+ echo "======================================" >> $TMPFILE
+
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ fi
+ return $ret
+}
+
+# arg1: hash function
+# arg2: key
+# arg3: text
+hmac() {
+ local hash_fn="$1"
+ local key="$2" text="$3" output
+ local -i ret
+
+ if [[ ! "$OSSL_NAME" =~ LibreSSL ]] && [[ $OSSL_VER_MAJOR.$OSSL_VER_MINOR == 3.0.0* ]]; then
+ output="$(asciihex_to_binary "$text" | $OPENSSL mac -macopt digest:"${hash_fn/-/}" -macopt hexkey:"$key" HMAC 2>/dev/null)"
+ ret=$?
+ tm_out "$(strip_lf "$output")"
+ else
+ output="$(asciihex_to_binary "$text" | $OPENSSL dgst "$hash_fn" -mac HMAC -macopt hexkey:"$key" 2>/dev/null)"
+ ret=$?
+ tm_out "$(awk '/=/ { print $2 }' <<< "$output")"
+ fi
+ return $ret
+}
+
+# arg1: hash function
+# arg2: pseudorandom key (PRK)
+# arg2: info
+# arg3: length of output keying material in octets
+# See RFC 5869, Section 2.3
+hkdf-expand() {
+ local hash_fn="$1"
+ local prk="$2" info="$3" output=""
+ local -i out_len="$4"
+ local -i i n mod_check hash_len ret
+ local counter
+ local ti tim1 # T(i) and T(i-1)
+
+ case "$hash_fn" in
+ "-sha256") hash_len=32 ;;
+ "-sha384") hash_len=48 ;;
+ *) return 7
+ esac
+
+ n=$out_len/$hash_len
+ mod_check=$out_len%$hash_len
+ [[ $mod_check -ne 0 ]] && n+=1
+
+ tim1=""
+ for (( i=1; i <= n; i++ )); do
+ counter="$(printf "%02X\n" $i)"
+ ti="$(hmac "$hash_fn" "$prk" "$tim1$info$counter")"
+ [[ $? -ne 0 ]] && return 7
+ output+="$ti"
+ tim1="$ti"
+ done
+ out_len=2*$out_len
+ tm_out "${output:0:out_len}"
+ return 0
+}
+
+# arg1: hash function
+# arg2: secret
+# arg3: label
+# arg4: context
+# arg5: length
+# See RFC 8446, Section 7.1
+hkdf-expand-label() {
+ local hash_fn="$1"
+ local secret="$2" label="$3"
+ local context="$4"
+ local -i length="$5"
+ local hkdflabel hkdflabel_label hkdflabel_context
+ local hkdflabel_length
+ local -i len
+
+ hkdflabel_length="$(printf "%04X\n" $length)"
+ if [[ "${TLS_SERVER_HELLO:8:2}" == "7F" ]] && [[ 0x${TLS_SERVER_HELLO:10:2} -lt 0x14 ]]; then
+ # "544c5320312e332c20" = "TLS 1.3, "
+ hkdflabel_label="544c5320312e332c20$label"
+ else
+ # "746c73313320" = "tls13 "
+ hkdflabel_label="746c73313320$label"
+ fi
+ len=${#hkdflabel_label}/2
+ hkdflabel_label="$(printf "%02X\n" $len)$hkdflabel_label"
+ len=${#context}/2
+ hkdflabel_context="$(printf "%02X\n" $len)$context"
+ hkdflabel="$hkdflabel_length$hkdflabel_label$hkdflabel_context"
+
+ hkdf-expand "$hash_fn" "$secret" "$hkdflabel" "$length"
+ return $?
+}
+
+# arg1: hash function
+# arg2: secret
+# arg3: label
+# arg4: ASCII-HEX of messages
+# See RFC 8446, Section 7.1
+derive-secret() {
+ local hash_fn="$1"
+ local secret="$2" label="$3" messages="$4"
+ local hash_messages
+ local -i hash_len retcode
+
+ case "$hash_fn" in
+ "-sha256") hash_len=32 ;;
+ "-sha384") hash_len=48 ;;
+ *) return 7
+ esac
+
+ hash_messages="$(asciihex_to_binary "$messages" | $OPENSSL dgst "$hash_fn" 2>/dev/null | awk '/=/ { print $2 }')"
+ hkdf-expand-label "$hash_fn" "$secret" "$label" "$hash_messages" "$hash_len"
+ return $?
+}
+
+# arg1: hash function
+# arg2: private key file
+# arg3: file containing server's ephemeral public key
+# arg4: ASCII-HEX of messages (ClientHello...ServerHello)
+# See key derivation schedule diagram in Section 7.1 of RFC 8446
+derive-handshake-traffic-secret() {
+ local hash_fn="$1"
+ local priv_file="$2" pub_file="$3"
+ local messages="$4"
+ local -i i ret
+ local early_secret derived_secret shared_secret handshake_secret
+
+ "$HAS_PKUTIL" || return 1
+
+ # early_secret="$(hmac "$hash_fn" "000...000" "000...000")"
+ case "$hash_fn" in
+ "-sha256") early_secret="33ad0a1c607ec03b09e6cd9893680ce210adf300aa1f2660e1b22e10f170f92a"
+ if [[ "${TLS_SERVER_HELLO:8:2}" == "7F" ]] && [[ 0x${TLS_SERVER_HELLO:10:2} -lt 0x14 ]]; then
+ # "6465726976656420736563726574" = "derived secret"
+ # derived_secret="$(derive-secret "$hash_fn" "$early_secret" "6465726976656420736563726574" "")"
+ derived_secret="c1c0c36bf8fb1d1afa949fbd360e71af69a6244a4c2eaef5bbbb6442a7277d2c"
+ else
+ # "64657269766564" = "derived"
+ # derived_secret="$(derive-secret "$hash_fn" "$early_secret" "64657269766564" "")"
+ derived_secret="6f2615a108c702c5678f54fc9dbab69716c076189c48250cebeac3576c3611ba"
+ fi
+ ;;
+ "-sha384") early_secret="7ee8206f5570023e6dc7519eb1073bc4e791ad37b5c382aa10ba18e2357e716971f9362f2c2fe2a76bfd78dfec4ea9b5"
+ if [[ "${TLS_SERVER_HELLO:8:2}" == "7F" ]] && [[ 0x${TLS_SERVER_HELLO:10:2} -lt 0x14 ]]; then
+ # "6465726976656420736563726574" = "derived secret"
+ # derived_secret="$(derive-secret "$hash_fn" "$early_secret" "6465726976656420736563726574" "")"
+ derived_secret="54c80fa05ee9e0532ce3db8ddeca37a0365683bcd3b27bdc88d2b9fdc115ca4ebc8edc1f0b72a6a0861e803fc34761ef"
+ else
+ # "64657269766564" = "derived"
+ # derived_secret="$(derive-secret "$hash_fn" "$early_secret" "64657269766564" "")"
+ derived_secret="1591dac5cbbf0330a4a84de9c753330e92d01f0a88214b4464972fd668049e93e52f2b16fad922fdc0584478428f282b"
+ fi
+ ;;
+ *) return 7
+ esac
+
+ shared_secret="$($OPENSSL pkeyutl -derive -inkey "$priv_file" -peerkey "$pub_file" 2>/dev/null | hexdump -v -e '16/1 "%02X"')"
+
+ # For draft 18 use $early_secret rather than $derived_secret.
+ if [[ "${TLS_SERVER_HELLO:8:4}" == "7F12" ]]; then
+ handshake_secret="$(hmac "$hash_fn" "$early_secret" "${shared_secret%%[!0-9A-F]*}")"
+ else
+ handshake_secret="$(hmac "$hash_fn" "$derived_secret" "${shared_secret%%[!0-9A-F]*}")"
+ fi
+ [[ $? -ne 0 ]] && return 7
+
+ if [[ "${TLS_SERVER_HELLO:8:2}" == "7F" ]] && [[ 0x${TLS_SERVER_HELLO:10:2} -lt 0x14 ]]; then
+ # "7365727665722068616e647368616b65207472616666696320736563726574" = "server handshake traffic secret"
+ derived_secret="$(derive-secret "$hash_fn" "$handshake_secret" "7365727665722068616e647368616b65207472616666696320736563726574" "$messages")"
+ else
+ # "732068732074726166666963" = "s hs traffic"
+ derived_secret="$(derive-secret "$hash_fn" "$handshake_secret" "732068732074726166666963" "$messages")"
+ fi
+ [[ $? -ne 0 ]] && return 7
+ tm_out "$derived_secret"
+ return 0
+}
+
+# arg1: hash function
+# arg2: secret (created by derive-handshake-traffic-secret)
+# arg3: purpose ("key" or "iv")
+# arg4: length of the key
+# See RFC 8446, Section 7.3
+derive-traffic-key() {
+ local hash_fn="$1"
+ local secret="$2" purpose="$3"
+ local -i key_length="$4"
+ local key
+
+ key="$(hkdf-expand-label "$hash_fn" "$secret" "$purpose" "" "$key_length")"
+ [[ $? -ne 0 ]] && return 7
+ tm_out "$key"
+ return 0
+}
+
+#arg1: TLS cipher
+#arg2: file containing cipher name, public key, and private key
+#arg3: First ClientHello, if response was a HelloRetryRequest
+#arg4: HelloRetryRequest, if one was sent
+#arg5: Final (or only) ClientHello
+#arg6: ServerHello
+derive-handshake-traffic-keys() {
+ local cipher="$1"
+ local tmpfile="$2"
+ local clienthello1="$3" hrr="$4" clienthello2="$5" serverhello="$6"
+ local hash_clienthello1
+ local -i key_len
+ local -i retcode
+ local hash_fn
+ local pub_file priv_file tmpfile
+ local derived_secret server_write_key server_write_iv
+
+ if [[ "$cipher" == *SHA256 ]]; then
+ hash_fn="-sha256"
+ elif [[ "$cipher" == *SHA384 ]]; then
+ hash_fn="-sha384"
+ else
+ return 1
+ fi
+ if [[ "$cipher" == *AES_128* ]]; then
+ key_len=16
+ elif ( [[ "$cipher" == *AES_256* ]] || [[ "$cipher" == *CHACHA20_POLY1305* ]] ); then
+ key_len=32
+ else
+ return 1
+ fi
+ pub_file="$(mktemp "$TEMPDIR/pubkey.XXXXXX")" || return 7
+ awk '/-----BEGIN PUBLIC KEY/,/-----END PUBLIC KEY/ { print $0 }' \
+ "$tmpfile" > "$pub_file"
+ [[ ! -s "$pub_file" ]] && return 1
+
+ priv_file="$(mktemp "$TEMPDIR/privkey.XXXXXX")" || return 7
+ if grep -q "\-\-\-\-\-BEGIN EC PARAMETERS" "$tmpfile"; then
+ awk '/-----BEGIN EC PARAMETERS/,/-----END EC PRIVATE KEY/ { print $0 }' \
+ "$tmpfile" > "$priv_file"
+ else
+ awk '/-----BEGIN PRIVATE KEY/,/-----END PRIVATE KEY/ { print $0 }' \
+ "$tmpfile" > "$priv_file"
+ fi
+ [[ ! -s "$priv_file" ]] && return 1
+
+ if [[ -n "$hrr" ]] && [[ "${serverhello:8:4}" == "7F12" ]]; then
+ derived_secret="$(derive-handshake-traffic-secret "$hash_fn" "$priv_file" "$pub_file" "$clienthello1$hrr$clienthello2$serverhello")"
+ elif [[ -n "$hrr" ]]; then
+ hash_clienthello1="$(asciihex_to_binary "$clienthello1" | $OPENSSL dgst "$hash_fn" 2>/dev/null | awk '/=/ { print $2 }')"
+ derived_secret="$(derive-handshake-traffic-secret "$hash_fn" "$priv_file" "$pub_file" "FE0000$(printf "%02x" $((${#hash_clienthello1}/2)))$hash_clienthello1$hrr$clienthello2$serverhello")"
+ else
+ derived_secret="$(derive-handshake-traffic-secret "$hash_fn" "$priv_file" "$pub_file" "$clienthello2$serverhello")"
+ fi
+ retcode=$?
+ rm $pub_file $priv_file
+ [[ $retcode -ne 0 ]] && return 1
+ # "6b6579" = "key"
+ server_write_key="$(derive-traffic-key "$hash_fn" "$derived_secret" "6b6579" "$key_len")"
+ [[ $? -ne 0 ]] && return 1
+ # "6976" = "iv"
+ server_write_iv="$(derive-traffic-key "$hash_fn" "$derived_secret" "6976" "12")"
+ [[ $? -ne 0 ]] && return 1
+ tm_out "$server_write_key $server_write_iv"
+ return 0
+}
+
+generate-ccm-gcm-keystream() {
+ local icb="$1" icb_msb icb_lsb1
+ local -i i icb_lsb n="$2"
+
+ icb_msb="${icb:0:24}"
+ icb_lsb=0x${icb:24:8}
+
+ for (( i=0; i < n; i=i+1 )); do
+ icb_lsb1="$(printf "%08X" $icb_lsb)"
+ printf "\x${icb_msb:0:2}\x${icb_msb:2:2}\x${icb_msb:4:2}\x${icb_msb:6:2}\x${icb_msb:8:2}\x${icb_msb:10:2}\x${icb_msb:12:2}\x${icb_msb:14:2}\x${icb_msb:16:2}\x${icb_msb:18:2}\x${icb_msb:20:2}\x${icb_msb:22:2}\x${icb_lsb1:0:2}\x${icb_lsb1:2:2}\x${icb_lsb1:4:2}\x${icb_lsb1:6:2}"
+ icb_lsb+=1
+ done
+ return 0
+}
+
+# arg1: an OpenSSL ecb cipher (e.g., -aes-128-ecb)
+# arg2: key
+# arg3: initial counter value (must be 128 bits)
+# arg4: ciphertext
+# See Sections 6.5 and 7.2 of SP 800-38D and Section 6.2 and Appendix A of SP 800-38C
+ccm-gcm-decrypt() {
+ local cipher="$1"
+ local key="$2"
+ local icb="$3"
+ local ciphertext="$4"
+ local -i i i1 i2 i3 i4
+ local -i ciphertext_len n mod_check
+ local y plaintext=""
+
+ [[ ${#icb} -ne 32 ]] && return 7
+
+ ciphertext_len=${#ciphertext}
+ n=$ciphertext_len/32
+ mod_check=$ciphertext_len%32
+ [[ $mod_check -ne 0 ]] && n+=1
+ y="$(generate-ccm-gcm-keystream "$icb" "$n" | $OPENSSL enc "$cipher" -K "$key" -nopad 2>/dev/null | hexdump -v -e '16/1 "%02X"')"
+
+ # XOR the ciphertext with the keystream ($y). For efficiency, work in blocks of 16 bytes at a time (but with each XOR operation working on
+ # 32 bits.
+ [[ $mod_check -ne 0 ]] && n=$n-1
+ for (( i=0; i < n; i++ )); do
+ i1=32*$i; i2=$i1+8; i3=$i1+16; i4=$i1+24
+ plaintext+="$(printf "%08X%08X%08X%08X" "$((0x${ciphertext:i1:8} ^ 0x${y:i1:8}))" "$((0x${ciphertext:i2:8} ^ 0x${y:i2:8}))" "$((0x${ciphertext:i3:8} ^ 0x${y:i3:8}))" "$((0x${ciphertext:i4:8} ^ 0x${y:i4:8}))")"
+ done
+ # If the length of the ciphertext is not an even multiple of 16 bytes, then handle the final incomplete block.
+ if [[ $mod_check -ne 0 ]]; then
+ i1=32*$n
+ for (( i=0; i < mod_check; i=i+2 )); do
+ plaintext+="$(printf "%02X" "$((0x${ciphertext:i1:2} ^ 0x${y:i1:2}))")"
+ i1+=2
+ done
+ fi
+ tm_out "$plaintext"
+ return 0
+}
+
+# See RFC 7539, Section 2.1
+chacha20_Qround() {
+ local -i a="0x$1"
+ local -i b="0x$2"
+ local -i c="0x$3"
+ local -i d="0x$4"
+ local -i x y
+
+ a=$(((a+b) & 0xffffffff))
+ d=$((d^a))
+ # rotate d left 16 bits
+ x=$((d & 0xffff0000))
+ x=$((x >> 16))
+ y=$((d & 0x0000ffff))
+ y=$((y << 16))
+ d=$((x | y))
+
+ c=$(((c+d) & 0xffffffff))
+ b=$((b^c))
+ # rotate b left 12 bits
+ x=$((b & 0xfff00000))
+ x=$((x >> 20))
+ y=$((b & 0x000fffff))
+ y=$((y << 12))
+ b=$((x | y))
+
+ a=$(((a+b) & 0xffffffff))
+ d=$((d^a))
+ # rotate d left 8 bits
+ x=$((d & 0xff000000))
+ x=$((x >> 24))
+ y=$((d & 0x00ffffff))
+ y=$((y << 8))
+ d=$((x | y))
+
+ c=$(((c+d) & 0xffffffff))
+ b=$((b^c))
+ # rotate b left 7 bits
+ x=$((b & 0xfe000000))
+ x=$((x >> 25))
+ y=$((b & 0x01ffffff))
+ y=$((y << 7))
+ b=$((x | y))
+
+ tm_out "$(printf "%x" $a) $(printf "%x" $b) $(printf "%x" $c) $(printf "%x" $d)"
+ return 0
+}
+
+# See RFC 7539, Section 2.3.1
+chacha20_inner_block() {
+ local s0="$1" s1="$2" s2="$3" s3="$4"
+ local s4="$5" s5="$6" s6="$7" s7="$8"
+ local s8="$9" s9="${10}" s10="${11}" s11="${12}"
+ local s12="${13}" s13="${14}" s14="${15}" s15="${16}"
+ local res
+
+ res="$(chacha20_Qround "$s0" "$s4" "$s8" "$s12")"
+ read -r s0 s4 s8 s12 <<< "$res"
+ res="$(chacha20_Qround "$s1" "$s5" "$s9" "$s13")"
+ read -r s1 s5 s9 s13 <<< "$res"
+ res="$(chacha20_Qround "$s2" "$s6" "$s10" "$s14")"
+ read -r s2 s6 s10 s14 <<< "$res"
+ res="$(chacha20_Qround "$s3" "$s7" "$s11" "$s15")"
+ read -r s3 s7 s11 s15 <<< "$res"
+ res="$(chacha20_Qround "$s0" "$s5" "$s10" "$s15")"
+ read -r s0 s5 s10 s15 <<< "$res"
+ res="$(chacha20_Qround "$s1" "$s6" "$s11" "$s12")"
+ read -r s1 s6 s11 s12 <<< "$res"
+ res="$(chacha20_Qround "$s2" "$s7" "$s8" "$s13")"
+ read -r s2 s7 s8 s13 <<< "$res"
+ res="$(chacha20_Qround "$s3" "$s4" "$s9" "$s14")"
+ read -r s3 s4 s9 s14 <<< "$res"
+
+ tm_out "$s0 $s1 $s2 $s3 $s4 $s5 $s6 $s7 $s8 $s9 $s10 $s11 $s12 $s13 $s14 $s15"
+ return 0
+}
+
+# See RFC 7539, Sections 2.3 and 2.3.1
+chacha20_block() {
+ local key="$1"
+ local counter="$2"
+ local nonce="$3"
+ local s0 s1 s2 s3 s4 s5 s6 s7 s8 s9 s10 s11 s12 s13 s14 s15
+ local ws0 ws1 ws2 ws3 ws4 ws5 ws6 ws7 ws8 ws9 ws10 ws11 ws12 ws13 ws14 ws15
+ local working_state
+ local -i i
+
+ # create the state variable
+ s0="61707865"; s1="3320646e"; s2="79622d32"; s3="6b206574"
+ s4="${key:6:2}${key:4:2}${key:2:2}${key:0:2}"
+ s5="${key:14:2}${key:12:2}${key:10:2}${key:8:2}"
+ s6="${key:22:2}${key:20:2}${key:18:2}${key:16:2}"
+ s7="${key:30:2}${key:28:2}${key:26:2}${key:24:2}"
+ s8="${key:38:2}${key:36:2}${key:34:2}${key:32:2}"
+ s9="${key:46:2}${key:44:2}${key:42:2}${key:40:2}"
+ s10="${key:54:2}${key:52:2}${key:50:2}${key:48:2}"
+ s11="${key:62:2}${key:60:2}${key:58:2}${key:56:2}"
+ s12="$counter"
+ s13="${nonce:6:2}${nonce:4:2}${nonce:2:2}${nonce:0:2}"
+ s14="${nonce:14:2}${nonce:12:2}${nonce:10:2}${nonce:8:2}"
+ s15="${nonce:22:2}${nonce:20:2}${nonce:18:2}${nonce:16:2}"
+
+ # Initialize working_state to state
+ working_state="$s0 $s1 $s2 $s3 $s4 $s5 $s6 $s7 $s8 $s9 $s10 $s11 $s12 $s13 $s14 $s15"
+
+ # compute the 20 rounds (10 calls to inner block function, each of which
+ # performs 8 quarter rounds).
+ for (( i=0 ; i < 10; i++ )); do
+ working_state="$(chacha20_inner_block $working_state)"
+ done
+ read -r ws0 ws1 ws2 ws3 ws4 ws5 ws6 ws7 ws8 ws9 ws10 ws11 ws12 ws13 ws14 ws15 <<< "$working_state"
+
+ # Add working state to state
+ s0="$(printf "%08X" $(((0x$s0+0x$ws0) & 0xffffffff)))"
+ s1="$(printf "%08X" $(((0x$s1+0x$ws1) & 0xffffffff)))"
+ s2="$(printf "%08X" $(((0x$s2+0x$ws2) & 0xffffffff)))"
+ s3="$(printf "%08X" $(((0x$s3+0x$ws3) & 0xffffffff)))"
+ s4="$(printf "%08X" $(((0x$s4+0x$ws4) & 0xffffffff)))"
+ s5="$(printf "%08X" $(((0x$s5+0x$ws5) & 0xffffffff)))"
+ s6="$(printf "%08X" $(((0x$s6+0x$ws6) & 0xffffffff)))"
+ s7="$(printf "%08X" $(((0x$s7+0x$ws7) & 0xffffffff)))"
+ s8="$(printf "%08X" $(((0x$s8+0x$ws8) & 0xffffffff)))"
+ s9="$(printf "%08X" $(((0x$s9+0x$ws9) & 0xffffffff)))"
+ s10="$(printf "%08X" $(((0x$s10+0x$ws10) & 0xffffffff)))"
+ s11="$(printf "%08X" $(((0x$s11+0x$ws11) & 0xffffffff)))"
+ s12="$(printf "%08X" $(((0x$s12+0x$ws12) & 0xffffffff)))"
+ s13="$(printf "%08X" $(((0x$s13+0x$ws13) & 0xffffffff)))"
+ s14="$(printf "%08X" $(((0x$s14+0x$ws14) & 0xffffffff)))"
+ s15="$(printf "%08X" $(((0x$s15+0x$ws15) & 0xffffffff)))"
+
+ # serialize the state
+ s0="${s0:6:2}${s0:4:2}${s0:2:2}${s0:0:2}"
+ s1="${s1:6:2}${s1:4:2}${s1:2:2}${s1:0:2}"
+ s2="${s2:6:2}${s2:4:2}${s2:2:2}${s2:0:2}"
+ s3="${s3:6:2}${s3:4:2}${s3:2:2}${s3:0:2}"
+ s4="${s4:6:2}${s4:4:2}${s4:2:2}${s4:0:2}"
+ s5="${s5:6:2}${s5:4:2}${s5:2:2}${s5:0:2}"
+ s6="${s6:6:2}${s6:4:2}${s6:2:2}${s6:0:2}"
+ s7="${s7:6:2}${s7:4:2}${s7:2:2}${s7:0:2}"
+ s8="${s8:6:2}${s8:4:2}${s8:2:2}${s8:0:2}"
+ s9="${s9:6:2}${s9:4:2}${s9:2:2}${s9:0:2}"
+ s10="${s10:6:2}${s10:4:2}${s10:2:2}${s10:0:2}"
+ s11="${s11:6:2}${s11:4:2}${s11:2:2}${s11:0:2}"
+ s12="${s12:6:2}${s12:4:2}${s12:2:2}${s12:0:2}"
+ s13="${s13:6:2}${s13:4:2}${s13:2:2}${s13:0:2}"
+ s14="${s14:6:2}${s14:4:2}${s14:2:2}${s14:0:2}"
+ s15="${s15:6:2}${s15:4:2}${s15:2:2}${s15:0:2}"
+
+ tm_out "$s0$s1$s2$s3$s4$s5$s6$s7$s8$s9$s10$s11$s12$s13$s14$s15"
+ return 0
+}
+
+# See RFC 7539, Section 2.4
+chacha20() {
+ local key="$1"
+ local -i counter=1
+ local nonce="$2"
+ local ciphertext="$3"
+ local -i i ciphertext_len num_blocks mod_check
+ local -i i1 i2 i3 i4 i5 i6 i7 i8 i9 i10 i11 i12 i13 i14 i15 i16
+ local keystream plaintext=""
+
+ ciphertext_len=${#ciphertext}
+ num_blocks=$ciphertext_len/128
+
+ for (( i=0; i < num_blocks; i++)); do
+ i1=128*$i; i2=$i1+8; i3=$i1+16; i4=$i1+24; i5=$i1+32; i6=$i1+40; i7=$i1+48; i8=$i1+56
+ i9=$i1+64; i10=$i1+72; i11=$i1+80; i12=$i1+88; i13=$i1+96; i14=$i1+104; i15=$i1+112; i16=$i1+120
+ keystream="$(chacha20_block "$key" "$(printf "%08X" $counter)" "$nonce")"
+ plaintext+="$(printf "%08X%08X%08X%08X%08X%08X%08X%08X%08X%08X%08X%08X%08X%08X%08X%08X" \
+ "$((0x${ciphertext:i1:8} ^ 0x${keystream:0:8}))" \
+ "$((0x${ciphertext:i2:8} ^ 0x${keystream:8:8}))" \
+ "$((0x${ciphertext:i3:8} ^ 0x${keystream:16:8}))" \
+ "$((0x${ciphertext:i4:8} ^ 0x${keystream:24:8}))" \
+ "$((0x${ciphertext:i5:8} ^ 0x${keystream:32:8}))" \
+ "$((0x${ciphertext:i6:8} ^ 0x${keystream:40:8}))" \
+ "$((0x${ciphertext:i7:8} ^ 0x${keystream:48:8}))" \
+ "$((0x${ciphertext:i8:8} ^ 0x${keystream:56:8}))" \
+ "$((0x${ciphertext:i9:8} ^ 0x${keystream:64:8}))" \
+ "$((0x${ciphertext:i10:8} ^ 0x${keystream:72:8}))" \
+ "$((0x${ciphertext:i11:8} ^ 0x${keystream:80:8}))" \
+ "$((0x${ciphertext:i12:8} ^ 0x${keystream:88:8}))" \
+ "$((0x${ciphertext:i13:8} ^ 0x${keystream:96:8}))" \
+ "$((0x${ciphertext:i14:8} ^ 0x${keystream:104:8}))" \
+ "$((0x${ciphertext:i15:8} ^ 0x${keystream:112:8}))" \
+ "$((0x${ciphertext:i16:8} ^ 0x${keystream:120:8}))")"
+ counter+=1
+ done
+
+ mod_check=$ciphertext_len%128
+ if [[ $mod_check -ne 0 ]]; then
+ keystream="$(chacha20_block "$key" "$(printf "%08X" $counter)" "$nonce")"
+ i1=128*$num_blocks
+ for (( i=0; i < mod_check; i=i+2 )); do
+ plaintext+="$(printf "%02X" "$((0x${ciphertext:i1:2} ^ 0x${keystream:i:2}))")"
+ i1+=2
+ done
+ fi
+ tm_out "$plaintext"
+ return 0
+}
+
+# arg1: TLS cipher
+# arg2: key
+# arg3: nonce (must be 96 bits in length)
+# arg4: ciphertext
+sym-decrypt() {
+ local cipher="$1"
+ local key="$2" nonce="$3"
+ local ciphertext="$4"
+ local ossl_cipher
+ local plaintext
+ local -i ciphertext_len tag_len
+
+ case "$cipher" in
+ *CCM_8*)
+ tag_len=16 ;;
+ *CCM*|*GCM*|*CHACHA20_POLY1305*)
+ tag_len=32 ;;
+ *)
+ return 7 ;;
+ esac
+
+ # The final $tag_len characters of the ciphertext are the authentication tag
+ ciphertext_len=${#ciphertext}
+ [[ $ciphertext_len -lt $tag_len ]] && return 7
+ ciphertext_len=$ciphertext_len-$tag_len
+
+ if [[ "$cipher" =~ CHACHA20_POLY1305 ]]; then
+ if "$HAS_CHACHA20"; then
+ plaintext="$(asciihex_to_binary "${ciphertext:0:ciphertext_len}" | \
+ $OPENSSL enc -chacha20 -K "$key" -iv "01000000$nonce" 2>/dev/null | hexdump -v -e '16/1 "%02X"')"
+ plaintext="$(strip_spaces "$plaintext")"
+ else
+ plaintext="$(chacha20 "$key" "$nonce" "${ciphertext:0:ciphertext_len}")"
+ fi
+ elif [[ "$cipher" == TLS_AES_128_GCM_SHA256 ]] && "$HAS_AES128_GCM"; then
+ plaintext="$(asciihex_to_binary "${ciphertext:0:ciphertext_len}" | \
+ $OPENSSL enc -aes-128-gcm -K "$key" -iv "$nonce" 2>/dev/null | hexdump -v -e '16/1 "%02X"')"
+ plaintext="$(strip_spaces "$plaintext")"
+ elif [[ "$cipher" == TLS_AES_256_GCM_SHA384 ]] && "$HAS_AES256_GCM"; then
+ plaintext="$(asciihex_to_binary "${ciphertext:0:ciphertext_len}" | \
+ $OPENSSL enc -aes-256-gcm -K "$key" -iv "$nonce" 2>/dev/null | hexdump -v -e '16/1 "%02X"')"
+ plaintext="$(strip_spaces "$plaintext")"
+ else
+ if [[ "$cipher" =~ AES_128 ]]; then
+ ossl_cipher="-aes-128-ecb"
+ elif [[ "$cipher" =~ AES_256 ]]; then
+ ossl_cipher="-aes-256-ecb"
+ else
+ return 7
+ fi
+ if [[ "$cipher" =~ CCM ]]; then
+ plaintext="$(ccm-gcm-decrypt "$ossl_cipher" "$key" "02${nonce}000001" "${ciphertext:0:ciphertext_len}")"
+ else # GCM
+ plaintext="$(ccm-gcm-decrypt "$ossl_cipher" "$key" "${nonce}00000002" "${ciphertext:0:ciphertext_len}")"
+ fi
+ fi
+ [[ $? -ne 0 ]] && return 7
+
+ tm_out "$plaintext"
+ return 0
+}
+
+# arg1: iv
+# arg2: sequence number
+get-nonce() {
+ local iv="$1"
+ local -i seq_num="$2"
+ local -i len lsb
+ local msb nonce
+
+ len=${#iv}
+ [[ $len -lt 8 ]] && return 7
+ i=$len-8
+ msb="${iv:0:i}"
+ lsb="0x${iv:i:8}"
+ nonce="${msb}$(printf "%08X" "$((lsb ^ seq_num))")"
+ tm_out "$nonce"
+ return 0
+}
+
+# Return:
+# 0 if arg1 contains the entire server response.
+# 1 if arg1 does not contain the entire server response.
+# 2 if the response is malformed.
+# 3 if (a) the response version is TLSv1.3;
+# (b) arg1 contains the entire ServerHello (and appears to contain the entire response);
+# (c) the entire response is supposed to be parsed; and
+# (d) the key and IV have not been provided to decrypt the response.
+# arg1: ASCII-HEX encoded reply
+# arg2: whether to process the full request ("all") or just the basic request plus the ephemeral key if any ("ephemeralkey").
+# arg3: TLS cipher for decrypting TLSv1.3 response
+# arg4: key and IV for decrypting TLSv1.3 response
+check_tls_serverhellodone() {
+ local tls_hello_ascii="$1"
+ local process_full="$2"
+ local cipher="$3"
+ local key_and_iv="$4"
+ local tls_handshake_ascii="" tls_alert_ascii=""
+ local -i i tls_hello_ascii_len tls_handshake_ascii_len tls_alert_ascii_len
+ local -i msg_len remaining tls_serverhello_ascii_len sid_len
+ local -i j offset tls_extensions_len extension_len
+ local tls_content_type tls_protocol tls_handshake_type tls_msg_type extension_type
+ local tls_err_level
+ local key iv
+ local -i seq_num=0 plaintext_len
+ local plaintext decrypted_response=""
+
+ DETECTED_TLS_VERSION=""
+
+ [[ -n "$key_and_iv" ]] && read -r key iv <<< "$key_and_iv"
+
+ if [[ -z "$tls_hello_ascii" ]]; then
+ return 0 # no server hello received
+ fi
+
+ tls_hello_ascii_len=${#tls_hello_ascii}
+ for (( i=0; i<tls_hello_ascii_len; i=i+msg_len )); do
+ remaining=$tls_hello_ascii_len-$i
+ [[ $remaining -lt 10 ]] && return 1
+
+ tls_content_type="${tls_hello_ascii:i:2}"
+ [[ "$tls_content_type" != 14 ]] && [[ "$tls_content_type" != 15 ]] && \
+ [[ "$tls_content_type" != 16 ]] && [[ "$tls_content_type" != 17 ]] && return 2
+ i=$i+2
+ tls_protocol="${tls_hello_ascii:i:4}"
+ [[ -z "$DETECTED_TLS_VERSION" ]] && DETECTED_TLS_VERSION="$tls_protocol"
+ [[ "${tls_protocol:0:2}" != 03 ]] && return 2
+ i=$i+4
+ msg_len=2*$(hex2dec "${tls_hello_ascii:i:4}")
+ i=$i+4
+ remaining=$tls_hello_ascii_len-$i
+ [[ $msg_len -gt $remaining ]] && return 1
+
+ if [[ "$tls_content_type" == 16 ]]; then
+ tls_handshake_ascii+="${tls_hello_ascii:i:msg_len}"
+ tls_handshake_ascii_len=${#tls_handshake_ascii}
+ decrypted_response+="$tls_content_type$tls_protocol$(printf "%04X" $((msg_len/2)))${tls_hello_ascii:i:msg_len}"
+ # the ServerHello MUST be the first handshake message
+ [[ $tls_handshake_ascii_len -ge 2 ]] && [[ "${tls_handshake_ascii:0:2}" != 02 ]] && return 2
+ if [[ $tls_handshake_ascii_len -ge 12 ]]; then
+ DETECTED_TLS_VERSION="${tls_handshake_ascii:8:4}"
+
+ # In TLSv1.3 (starting with draft 22), the version field specifies TLSv1.2, but
+ # there is a supported_versions extension that specifies the actual version. So,
+ # if the version field specifies TLSv1.2, then check to see if there is a
+ # supported_versions extension.
+ if [[ "$DETECTED_TLS_VERSION" == 0303 ]]; then
+ tls_serverhello_ascii_len=2*$(hex2dec "${tls_handshake_ascii:2:6}")
+ sid_len=2*$(hex2dec "${tls_handshake_ascii:76:2}")
+ if [[ $tls_serverhello_ascii_len -gt 76+$sid_len ]]; then
+ # ServerHello contains extensions, so check for supported_versions extension
+ offset=84+$sid_len
+ tls_extensions_len=2*$(hex2dec "${tls_handshake_ascii:offset:4}")
+ [[ $tls_extensions_len -ne $tls_serverhello_ascii_len-$sid_len-80 ]] && return 2
+ for (( j=0; j<tls_extensions_len; j=j+8+extension_len )); do
+ [[ $tls_extensions_len-$j -lt 8 ]] && return 2
+ offset=88+$sid_len+$j
+ extension_type="${tls_handshake_ascii:offset:4}"
+ offset=92+$sid_len+$j
+ extension_len=2*$(hex2dec "${tls_handshake_ascii:offset:4}")
+ [[ $extension_len -gt $tls_extensions_len-$j-8 ]] && return 2
+ if [[ "$extension_type" == 002B ]]; then # supported_versions
+ [[ $extension_len -ne 4 ]] && return 2
+ offset=96+$sid_len+$j
+ DETECTED_TLS_VERSION="${tls_handshake_ascii:offset:4}"
+ fi
+ done
+ fi
+ fi
+ # A version of {0x7F, xx} represents an implementation of a draft version of TLS 1.3
+ [[ "${DETECTED_TLS_VERSION:0:2}" == 7F ]] && DETECTED_TLS_VERSION=0304
+ if [[ 0x$DETECTED_TLS_VERSION -ge 0x0304 ]] && [[ "$process_full" == ephemeralkey ]]; then
+ tls_serverhello_ascii_len=2*$(hex2dec "${tls_handshake_ascii:2:6}")
+ if [[ $tls_handshake_ascii_len -ge $tls_serverhello_ascii_len+8 ]]; then
+ tm_out ""
+ return 0 # The entire ServerHello message has been received (and the rest isn't needed)
+ fi
+ fi
+ fi
+ elif [[ "$tls_content_type" == 15 ]]; then # TLS ALERT
+ tls_alert_ascii+="${tls_hello_ascii:i:msg_len}"
+ decrypted_response+="$tls_content_type$tls_protocol$(printf "%04X" $((msg_len/2)))${tls_hello_ascii:i:msg_len}"
+ elif [[ "$tls_content_type" == 17 ]] && [[ -n "$key_and_iv" ]]; then # encrypted data
+ nonce="$(get-nonce "$iv" "$seq_num")"
+ [[ $? -ne 0 ]] && return 2
+ plaintext="$(sym-decrypt "$cipher" "$key" "$nonce" "${tls_hello_ascii:i:msg_len}")"
+ [[ $? -ne 0 ]] && return 2
+ seq_num+=1
+
+ # Remove zeros from end of plaintext, if any
+ plaintext_len=${#plaintext}-2
+ while [[ "${plaintext:plaintext_len:2}" == 00 ]]; do
+ plaintext_len=$plaintext_len-2
+ done
+ tls_content_type="${plaintext:plaintext_len:2}"
+ decrypted_response+="${tls_content_type}0301$(printf "%04X" $((plaintext_len/2)))${plaintext:0:plaintext_len}"
+ if [[ "$tls_content_type" == 16 ]]; then
+ tls_handshake_ascii+="${plaintext:0:plaintext_len}"
+ elif [[ "$tls_content_type" == 15 ]]; then
+ tls_alert_ascii+="${plaintext:0:plaintext_len}"
+ else
+ return 2
+ fi
+ fi
+ done
+
+ # If there is a fatal alert, then we are done.
+ tls_alert_ascii_len=${#tls_alert_ascii}
+ for (( i=0; i<tls_alert_ascii_len; i=i+4 )); do
+ remaining=$tls_alert_ascii_len-$i
+ [[ $remaining -lt 4 ]] && return 1
+ tls_err_level=${tls_alert_ascii:i:2} # 1: warning, 2: fatal
+ [[ $tls_err_level == 02 ]] && DETECTED_TLS_VERSION="" && tm_out "" && return 0
+ done
+
+ # If there is a serverHelloDone or Finished, then we are done.
+ tls_handshake_ascii_len=${#tls_handshake_ascii}
+ for (( i=0; i<tls_handshake_ascii_len; i=i+msg_len )); do
+ remaining=$tls_handshake_ascii_len-$i
+ [[ $remaining -lt 8 ]] && return 1
+ tls_msg_type="${tls_handshake_ascii:i:2}"
+ i=$i+2
+ msg_len=2*$(hex2dec "${tls_handshake_ascii:i:6}")
+ i=$i+6
+ remaining=$tls_handshake_ascii_len-$i
+ [[ $msg_len -gt $remaining ]] && return 1
+
+ # For SSLv3 - TLS1.2 look for a ServerHelloDone message.
+ # For TLS 1.3 look for a Finished message.
+ [[ $tls_msg_type == 0E ]] && tm_out "" && return 0
+ [[ $tls_msg_type == 14 ]] && tm_out "$decrypted_response" && return 0
+ done
+ # If the response is TLSv1.3 and the full response is to be processed, but the
+ # key and IV have not been provided to decrypt the response, then return 3 if
+ # the entire ServerHello has been received.
+ if [[ "$DETECTED_TLS_VERSION" == 0304 ]] && [[ "$process_full" =~ all ]] && \
+ [[ -z "$key_and_iv" ]] && [[ $tls_handshake_ascii_len -gt 0 ]]; then
+ return 3
+ fi
+ # If we haven't encountered a fatal alert or a server hello done,
+ # then there must be more data to retrieve.
+ return 1
+}
+
+# arg1: tls alert error/warning code
+# returns: description
+tls_alert() {
+ local tls_alert_text=""
+
+ case "$1" in
+ 00) tls_alert_text="close notify" ;;
+ 0A) tls_alert_text="unexpected message" ;;
+ 14) tls_alert_text="bad record mac" ;;
+ 15) tls_alert_text="decryption failed" ;;
+ 16) tls_alert_text="record overflow" ;;
+ 1E) tls_alert_text="decompression failure" ;;
+ 28) tls_alert_text="handshake failure" ;;
+ 29) tls_alert_text="no certificate RESERVED" ;;
+ 2A) tls_alert_text="bad certificate" ;;
+ 2B) tls_alert_text="unsupported certificate" ;;
+ 2C) tls_alert_text="certificate revoked" ;;
+ 2D) tls_alert_text="certificate expired" ;;
+ 2E) tls_alert_text="certificate unknown" ;;
+ 2F) tls_alert_text="illegal parameter" ;;
+ 30) tls_alert_text="unknown ca" ;;
+ 31) tls_alert_text="access denied" ;;
+ 32) tls_alert_text="decode error" ;;
+ 33) tls_alert_text="decrypt error" ;;
+ 3C) tls_alert_text="export restriction RESERVED" ;;
+ 46) tls_alert_text="protocol version" ;;
+ 47) tls_alert_text="insufficient security" ;;
+ 50) tls_alert_text="internal error" ;;
+ 56) tls_alert_text="inappropriate fallback" ;;
+ 5A) tls_alert_text="user canceled" ;;
+ 64) tls_alert_text="no renegotiation" ;;
+ 6D) tls_alert_text="missing extension" ;;
+ 6E) tls_alert_text="unsupported extension" ;;
+ 6F) tls_alert_text="certificate unobtainable" ;;
+ 70) tls_alert_text="unrecognized name" ;;
+ 71) tls_alert_text="bad certificate status response" ;;
+ 72) tls_alert_text="bad certificate hash value" ;;
+ 73) tls_alert_text="unknown psk identity" ;;
+ 74) tls_alert_text="certificate required" ;;
+ 78) tls_alert_text="no application protocol" ;;
+ *) tls_alert_text="$(hex2dec "$1")";;
+ esac
+ echo "$tls_alert_text"
+ return 0
+}
+
+# arg1: ASCII-HEX encoded reply
+# arg2: (optional): "all" or "all+" - process full response (including Certificate and certificate_status handshake messages)
+# "ephemeralkey" - extract the server's ephemeral key (if any)
+# arg3: (optional): CIPHER_SUITES string (lowercase, and in the format output by code2network())
+# If present, parse_tls_serverhello() will check that the cipher in the ServerHello appears in
+# the CIPHER_SUITES string.
+parse_tls_serverhello() {
+ local tls_hello_ascii="$1"
+ local process_full="$2"
+ local cipherlist="$3"
+ local tls_handshake_ascii="" tls_alert_ascii=""
+ local -i tls_hello_ascii_len tls_handshake_ascii_len tls_alert_ascii_len msg_len
+ local tls_serverhello_ascii="" tls_certificate_ascii=""
+ local tls_serverkeyexchange_ascii="" tls_certificate_status_ascii=""
+ local tls_encryptedextensions_ascii="" tls_revised_certificate_msg=""
+ local -i tls_serverhello_ascii_len=0 tls_certificate_ascii_len=0
+ local -i tls_serverkeyexchange_ascii_len=0 tls_certificate_status_ascii_len=0
+ local -i tls_encryptedextensions_ascii_len=0
+ local added_encrypted_extensions=false
+ local tls_alert_descrip tls_sid_len_hex issuerDN subjectDN CAissuerDN CAsubjectDN
+ local -i tls_sid_len offset extns_offset nr_certs=0
+ local tls_msg_type tls_content_type tls_protocol tls_protocol2 tls_hello_time
+ local tls_err_level tls_err_descr_no tls_cipher_suite rfc_cipher_suite tls_compression_method
+ local tls_extensions="" extension_type named_curve_str="" named_curve_oid
+ local -i i j extension_len extn_len tls_extensions_len ocsp_response_len=0 ocsp_response_list_len ocsp_resp_offset
+ local -i certificate_list_len certificate_len cipherlist_len
+ local -i curve_type named_curve
+ local -i dh_bits=0 msb mask
+ local hostcert_issuer=""
+ local len1 len2 len3 key_bitstring="" pem_certificate
+ local dh_p dh_param ephemeral_param rfc7919_param
+ local -i dh_p_len dh_param_len
+
+ DETECTED_TLS_VERSION=""
+ [[ $DEBUG -ge 1 ]] && echo > $TMPFILE
+
+ [[ "$DEBUG" -ge 5 ]] && echo $tls_hello_ascii # one line without any blanks
+
+ # Client messages, including handshake messages, are carried by the record layer.
+ # First, extract the handshake and alert messages.
+ # see https://en.wikipedia.org/wiki/Transport_Layer_Security-SSL#TLS_record
+ # byte 0: content type: 0x14=CCS, 0x15=TLS alert x16=Handshake, 0x17 Application, 0x18=HB
+ # byte 1+2: TLS version word, major is 03, minor 00=SSL3, 01=TLS1 02=TLS1.1 03=TLS 1.2
+ # byte 3+4: fragment length
+ # bytes 5...: message fragment
+ tls_hello_ascii_len=${#tls_hello_ascii}
+ if [[ $DEBUG -ge 3 ]] && [[ $tls_hello_ascii_len -gt 0 ]]; then
+ echo "TLS message fragments:"
+ fi
+ for (( i=0; i<tls_hello_ascii_len; i=i+msg_len )); do
+ if [[ $tls_hello_ascii_len-$i -lt 10 ]]; then
+ if [[ "$process_full" =~ all ]]; then
+ # The entire server response should have been retrieved.
+ debugme tmln_warning "Malformed message."
+ [[ $DEBUG -ge 1 ]] && tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ else
+ # This could just be a result of the server's response being
+ # split across two or more packets.
+ break
+ fi
+ fi
+ tls_content_type="${tls_hello_ascii:i:2}"
+ i=$i+2
+ tls_protocol="${tls_hello_ascii:i:4}"
+ i=$i+4
+ msg_len=2*$(hex2dec "${tls_hello_ascii:i:4}")
+ i=$i+4
+
+ if [[ $DEBUG -ge 3 ]]; then
+ echo " protocol (rec. layer): 0x$tls_protocol"
+ echo -n " tls_content_type: 0x$tls_content_type"
+ case $tls_content_type in
+ 14) tmln_out " (change cipher spec)" ;;
+ 15) tmln_out " (alert)" ;;
+ 16) tmln_out " (handshake)" ;;
+ 17) tmln_out " (application data)" ;;
+ *) tmln_out ;;
+ esac
+ echo " msg_len: $((msg_len/2))"
+ tmln_out
+ fi
+
+ if "$do_starttls" ; then
+ if [[ $tls_content_type == 35 ]] || [[ $tls_content_type == 34 ]]; then
+ # STARTTLS handshake failed and server replied plaintext with a 5xx or 4xx
+ [[ $DEBUG -ge 2 ]] && printf "%s\n" "400/500: $(hex2ascii "$tls_hello_ascii" 2>/dev/null)"
+ [[ $DEBUG -ge 1 ]] && tmpfile_handle ${FUNCNAME[0]}.txt
+ return 4
+ elif [[ "$tls_hello_ascii" =~ 6130303220 ]]; then
+ [[ $DEBUG -ge 2 ]] && printf "%s\n" "probably IMAP plaintext reply \"$(hex2ascii "${tls_hello_ascii:0:32}" 2>/dev/null)\""
+ [[ $DEBUG -ge 1 ]] && tmpfile_handle ${FUNCNAME[0]}.txt
+ return 3
+ fi
+ fi
+ if [[ $tls_content_type != 14 ]] && [[ $tls_content_type != 15 ]] && \
+ [[ $tls_content_type != 16 ]] && [[ $tls_content_type != 17 ]]; then
+ debugme tmln_warning "Content type other than alert, handshake, change cipher spec, or application data detected."
+ [[ $DEBUG -ge 1 ]] && tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ elif [[ "${tls_protocol:0:2}" != 03 ]]; then
+ debugme tmln_warning "Protocol record_version.major is not 03."
+ [[ $DEBUG -ge 1 ]] && tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+ DETECTED_TLS_VERSION=$tls_protocol
+
+ if [[ $msg_len -gt $tls_hello_ascii_len-$i ]]; then
+ if [[ "$process_full" =~ all ]]; then
+ debugme tmln_warning "Malformed message."
+ [[ $DEBUG -ge 1 ]] && tmpfile_handle ${FUNCNAME[0]}.txt
+ return 7
+ else
+ # This could just be a result of the server's response being split
+ # across two or more packets. Just grab the part that is available.
+ msg_len=$tls_hello_ascii_len-$i
+ fi
+ fi
+
+ if [[ $tls_content_type == 16 ]]; then
+ tls_handshake_ascii="$tls_handshake_ascii${tls_hello_ascii:i:msg_len}"
+ elif [[ $tls_content_type == 15 ]]; then # TLS ALERT
+ tls_alert_ascii="$tls_alert_ascii${tls_hello_ascii:i:msg_len}"
+ fi
+ done
+
+ # Now check the alert messages.
+ tls_alert_ascii_len=${#tls_alert_ascii}
+ if [[ "$process_full" =~ all ]] && [[ $tls_alert_ascii_len%4 -ne 0 ]]; then
+ debugme tmln_warning "Malformed message."
+ [[ $DEBUG -ge 1 ]] && tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+
+ if [[ $tls_alert_ascii_len -gt 0 ]]; then
+ debugme echo "TLS alert messages:"
+ for (( i=0; i+3 < tls_alert_ascii_len; i=i+4 )); do
+ tls_err_level=${tls_alert_ascii:i:2} # 1: warning, 2: fatal
+ j=$i+2
+ tls_err_descr_no=${tls_alert_ascii:j:2}
+ if [[ $DEBUG -ge 1 ]]; then
+ debugme tm_out " tls_err_descr_no: 0x${tls_err_descr_no} / = $(hex2dec ${tls_err_descr_no})"
+ tls_alert_descrip="$(tls_alert "$tls_err_descr_no")"
+ if [[ $DEBUG -ge 2 ]]; then
+ tmln_out " ($tls_alert_descrip)"
+ tm_out " tls_err_level: ${tls_err_level}"
+ fi
+ case $tls_err_level in
+ 01) echo -n "warning " >> $TMPFILE
+ debugme tmln_out " (warning)" ;;
+ 02) echo -n "fatal " >> $TMPFILE
+ debugme tmln_out " (fatal)" ;;
+ esac
+ echo "alert $tls_alert_descrip" >> $TMPFILE
+ echo "===============================================================================" >> $TMPFILE
+ fi
+
+ if [[ "$tls_err_level" != 01 ]] && [[ "$tls_err_level" != 02 ]]; then
+ debugme tmln_warning "Unexpected AlertLevel (0x$tls_err_level)."
+ [[ $DEBUG -ge 1 ]] && tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ elif [[ "$tls_err_level" == 02 ]]; then
+ # Fatal alert
+ [[ $DEBUG -ge 1 ]] && tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+ done
+ fi
+
+ # Now extract just the server hello, certificate, certificate status,
+ # and server key exchange handshake messages.
+ tls_handshake_ascii_len=${#tls_handshake_ascii}
+ if [[ $DEBUG -ge 3 ]] && [[ $tls_handshake_ascii_len -gt 0 ]]; then
+ echo "TLS handshake messages:"
+ fi
+ for (( i=0; i<tls_handshake_ascii_len; i=i+msg_len )); do
+ if [[ $tls_handshake_ascii_len-$i -lt 8 ]]; then
+ if [[ "$process_full" =~ all ]]; then
+ # The entire server response should have been retrieved.
+ debugme tmln_warning "Malformed message."
+ [[ $DEBUG -ge 1 ]] && tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ else
+ # This could just be a result of the server's response being
+ # split across two or more packets.
+ continue
+ fi
+ fi
+ tls_msg_type="${tls_handshake_ascii:i:2}"
+ i=$i+2
+ msg_len=2*$(hex2dec "${tls_handshake_ascii:i:6}")
+ i=$i+6
+ if [[ $DEBUG -ge 3 ]]; then
+ tm_out " handshake type: 0x${tls_msg_type}"
+ case $tls_msg_type in
+ 00) tmln_out " (hello_request)" ;;
+ 01) tmln_out " (client_hello)" ;;
+ 02) tmln_out " (server_hello)" ;;
+ 03) tmln_out " (hello_verify_request)" ;;
+ 04) tmln_out " (new_session_ticket)" ;;
+ 05) tmln_out " (end_of_early_data)" ;;
+ 06) tmln_out " (hello_retry_request)" ;;
+ 08) tmln_out " (encrypted_extensions)" ;;
+ 0B) tmln_out " (certificate)" ;;
+ 0C) tmln_out " (server_key_exchange)" ;;
+ 0D) tmln_out " (certificate_request)" ;;
+ 0E) tmln_out " (server_hello_done)" ;;
+ 0F) tmln_out " (certificate_verify)" ;;
+ 10) tmln_out " (client_key_exchange)" ;;
+ 14) tmln_out " (finished)" ;;
+ 15) tmln_out " (certificate_url)" ;;
+ 16) tmln_out " (certificate_status)" ;;
+ 17) tmln_out " (supplemental_data)" ;;
+ 18) tmln_out " (key_update)" ;;
+ 19) tmln_out " (compressed_certificate)" ;;
+ FE) tmln_out " (message_hash)" ;;
+ *) tmln_out ;;
+ esac
+ echo " msg_len: $((msg_len/2))"
+ tmln_out
+ fi
+ if [[ $msg_len -gt $tls_handshake_ascii_len-$i ]]; then
+ if [[ "$process_full" =~ all ]]; then
+ debugme tmln_warning "Malformed message."
+ [[ $DEBUG -ge 1 ]] && tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ else
+ # This could just be a result of the server's response being
+ # split across two or more packets. Just grab the part that
+ # is available.
+ msg_len=$tls_handshake_ascii_len-$i
+ fi
+ fi
+
+ if [[ "$tls_msg_type" == 02 ]]; then
+ if [[ -n "$tls_serverhello_ascii" ]]; then
+ debugme tmln_warning "Response contained more than one ServerHello handshake message."
+ [[ $DEBUG -ge 1 ]] && tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+ tls_serverhello_ascii="${tls_handshake_ascii:i:msg_len}"
+ tls_serverhello_ascii_len=$msg_len
+ elif [[ "$process_full" =~ all ]] && [[ "$tls_msg_type" == 08 ]]; then
+ # Add excrypted extensions (now decrypted) to end of extensions in SeverHello
+ tls_encryptedextensions_ascii="${tls_handshake_ascii:i:msg_len}"
+ tls_encryptedextensions_ascii_len=$msg_len
+ if [[ $msg_len -lt 2 ]]; then
+ debugme tmln_warning "Response contained a malformed encrypted extensions message"
+ return 1
+ fi
+ elif [[ "$process_full" =~ all ]] && [[ "$tls_msg_type" == 0B ]]; then
+ if [[ -n "$tls_certificate_ascii" ]]; then
+ debugme tmln_warning "Response contained more than one Certificate handshake message."
+ [[ $DEBUG -ge 1 ]] && tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+ tls_certificate_ascii="${tls_handshake_ascii:i:msg_len}"
+ tls_certificate_ascii_len=$msg_len
+ elif ( [[ "$process_full" =~ all ]] || [[ "$process_full" == ephemeralkey ]] ) && [[ "$tls_msg_type" == 0C ]]; then
+ if [[ -n "$tls_serverkeyexchange_ascii" ]]; then
+ debugme tmln_warning "Response contained more than one ServerKeyExchange handshake message."
+ [[ $DEBUG -ge 1 ]] && tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+ tls_serverkeyexchange_ascii="${tls_handshake_ascii:i:msg_len}"
+ tls_serverkeyexchange_ascii_len=$msg_len
+ elif [[ "$process_full" =~ all ]] && [[ "$tls_msg_type" == 16 ]]; then
+ if [[ -n "$tls_certificate_status_ascii" ]]; then
+ debugme tmln_warning "Response contained more than one certificate_status handshake message."
+ [[ $DEBUG -ge 1 ]] && tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+ tls_certificate_status_ascii="${tls_handshake_ascii:i:msg_len}"
+ tls_certificate_status_ascii_len=$msg_len
+ elif [[ "$tls_msg_type" == 19 ]]; then
+ if [[ -n "$tls_certificate_ascii" ]]; then
+ debugme tmln_warning "Response contained more than one Certificate handshake message."
+ [[ $DEBUG -ge 1 ]] && tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+ if [[ $DEBUG -ge 3 ]]; then
+ tm_out " Certificate Compression Algorithm: ${tls_handshake_ascii:i:4}"
+ case ${tls_handshake_ascii:i:4} in
+ 0001) tmln_out " (ZLIB)" ;;
+ 0002) tmln_out " (Brotli)" ;;
+ 0003) tmln_out " (Zstandard)" ;;
+ *) tmln_out ;;
+ esac
+ offset=$((i+4))
+ tmln_out " Uncompressed certificate length: $(printf "%d" 0x${tls_handshake_ascii:offset:6})"
+ tmln_out
+ fi
+ if [[ "$process_full" =~ all ]] && "$HAS_ZLIB" && [[ "${tls_handshake_ascii:i:4}" == 0001 ]]; then
+ offset=$((i+4))
+ tls_certificate_ascii_len=2*0x${tls_handshake_ascii:offset:6}
+ offset=$((i+16))
+ len1=$((msg_len-16))
+ tls_certificate_ascii="$(asciihex_to_binary "${tls_handshake_ascii:offset:len1}" | $OPENSSL zlib -d 2>/dev/null | hexdump -v -e '16/1 "%02X"')"
+ tls_certificate_ascii="${tls_certificate_ascii%%[!0-9A-F]*}"
+ if [[ ${#tls_certificate_ascii} -ne $tls_certificate_ascii_len ]]; then
+ debugme tmln_warning "Length of uncompressed certificates did not match specified length."
+ return 1
+ fi
+ fi
+ fi
+ done
+
+ if [[ $tls_serverhello_ascii_len -eq 0 ]]; then
+ debugme echo "server hello empty, TCP connection closed"
+ DETECTED_TLS_VERSION="closed TCP connection "
+ [[ $DEBUG -ge 1 ]] && tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1 # no server hello received
+ elif [[ $tls_serverhello_ascii_len -lt 76 ]]; then
+ DETECTED_TLS_VERSION="reply malformed"
+ debugme echo "Malformed response"
+ [[ $DEBUG -ge 1 ]] && tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ elif [[ "${tls_handshake_ascii:0:2}" != 02 ]]; then
+ # the ServerHello MUST be the first handshake message
+ DETECTED_TLS_VERSION="reply contained no ServerHello"
+ debugme tmln_warning "The first handshake protocol message is not a ServerHello."
+ [[ $DEBUG -ge 1 ]] && tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+ if [[ $DEBUG -eq 0 ]]; then
+ echo "CONNECTED(00000003)" > $TMPFILE
+ else
+ echo "CONNECTED(00000003)" >> $TMPFILE
+ fi
+
+ # First parse the server hello handshake message
+ # byte 0+1: 03, TLS version word see byte 1+2
+ # byte 2-5: TLS timestamp for OpenSSL <1.01f
+ # byte 6-33: random, 28 bytes
+ # byte 34: session id length
+ # byte 35+36+sid-len: cipher suite!
+ # byte 37+sid-len: compression method: 00: none, 01: deflate, 64: LZS
+ # byte 38+39+sid-len: extension length
+ tls_protocol2="${tls_serverhello_ascii:0:4}"
+ DETECTED_TLS_VERSION="$tls_protocol2"
+ [[ "${DETECTED_TLS_VERSION:0:2}" == 7F ]] && DETECTED_TLS_VERSION="0304"
+ if [[ "${DETECTED_TLS_VERSION:0:2}" != 03 ]]; then
+ debugme tmln_warning "server_version.major in ServerHello is not 03."
+ [[ $DEBUG -ge 1 ]] && tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+
+ if [[ "0x${DETECTED_TLS_VERSION:2:2}" -le "0x03" ]]; then
+ tls_hello_time="${tls_serverhello_ascii:4:8}"
+ [[ "$TLS_DIFFTIME_SET" || "$DEBUG" ]] && TLS_TIME=$(hex2dec "$tls_hello_time")
+ tls_sid_len_hex="${tls_serverhello_ascii:68:2}"
+ tls_sid_len=2*$(hex2dec "$tls_sid_len_hex")
+ offset=$((tls_sid_len+70))
+ if [[ $tls_serverhello_ascii_len -lt 76+$tls_sid_len ]]; then
+ debugme echo "Malformed response"
+ [[ $DEBUG -ge 1 ]] && tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+ else
+ offset=68
+ fi
+
+ tls_cipher_suite="${tls_serverhello_ascii:offset:4}"
+
+ if [[ "0x${DETECTED_TLS_VERSION:2:2}" -le "0x03" ]]; then
+ offset=$((tls_sid_len+74))
+ tls_compression_method="${tls_serverhello_ascii:offset:2}"
+ extns_offset=$((tls_sid_len+76))
+ else
+ extns_offset=72
+ fi
+
+ if [[ $tls_serverhello_ascii_len -gt $extns_offset ]] && \
+ ( [[ "$process_full" =~ all ]] || [[ "$DETECTED_TLS_VERSION" == 0303 ]] || \
+ ( [[ "$process_full" == ephemeralkey ]] && [[ "0x${DETECTED_TLS_VERSION:2:2}" -gt "0x03" ]] ) ); then
+ if [[ $tls_serverhello_ascii_len -lt $extns_offset+4 ]]; then
+ debugme echo "Malformed response"
+ [[ $DEBUG -ge 1 ]] && tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+ tls_extensions_len=$(hex2dec "${tls_serverhello_ascii:extns_offset:4}")*2
+ if [[ $tls_extensions_len -ne $tls_serverhello_ascii_len-$extns_offset-4 ]]; then
+ debugme tmln_warning "Malformed message."
+ [[ $DEBUG -ge 1 ]] && tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+ for (( i=0; i<tls_extensions_len; i=i+8+extension_len )); do
+ if [[ $tls_extensions_len-$i -lt 8 ]]; then
+ debugme echo "Malformed response"
+ [[ $DEBUG -ge 1 ]] && tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+ offset=$((extns_offset+i+4))
+ extension_type="${tls_serverhello_ascii:offset:4}"
+ offset=$((extns_offset+i+8))
+ extension_len=2*$(hex2dec "${tls_serverhello_ascii:offset:4}")
+ if [[ $extension_len -gt $tls_extensions_len-$i-8 ]]; then
+ debugme echo "Malformed response"
+ [[ $DEBUG -ge 1 ]] && tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+ case $extension_type in
+ 0000) tls_extensions+="TLS server extension \"server name\" (id=0), len=$extension_len\n" ;;
+ 0001) tls_extensions+="TLS server extension \"max fragment length\" (id=1), len=$extension_len\n" ;;
+ 0002) tls_extensions+="TLS server extension \"client certificate URL\" (id=2), len=$extension_len\n" ;;
+ 0003) tls_extensions+="TLS server extension \"trusted CA keys\" (id=3, len=$extension_len\n)" ;;
+ 0004) tls_extensions+="TLS server extension \"truncated HMAC\" (id=4), len=$extension_len\n" ;;
+ 0005) tls_extensions+="TLS server extension \"status request\" (id=5), len=$extension_len\n"
+ if [[ $extension_len -gt 0 ]] && [[ "$process_full" =~ all ]]; then
+ # In TLSv1.3 the status_request extension contains the CertificateStatus message, unlike
+ # TLSv1.2 and below where CertificateStatus appears in its own handshake message. So, if
+ # the status_request extension is not empty, extract the value and place it in
+ # $tls_certificate_status_ascii.
+ tls_certificate_status_ascii_len=$extension_len
+ offset=$((extns_offset+12+i))
+ tls_certificate_status_ascii="${tls_serverhello_ascii:offset:tls_certificate_status_ascii_len}"
+ fi
+ ;;
+ 0006) tls_extensions+="TLS server extension \"user mapping\" (id=6), len=$extension_len\n" ;;
+ 0007) tls_extensions+="TLS server extension \"client authz\" (id=7), len=$extension_len\n" ;;
+ 0008) tls_extensions+="TLS server extension \"server authz\" (id=8), len=$extension_len\n" ;;
+ 0009) tls_extensions+="TLS server extension \"cert type\" (id=9), len=$extension_len\n" ;;
+ 000A) tls_extensions+="TLS server extension \"supported_groups\" (id=10), len=$extension_len\n"
+ if [[ "$process_full" =~ all ]]; then
+ if [[ $extension_len -lt 4 ]]; then
+ debugme tmln_warning "Malformed supported groups extension."
+ return 1
+ fi
+ echo -n "Supported groups: " >> $TMPFILE
+ offset=$((extns_offset+12+i))
+ len1=2*$(hex2dec "${tls_serverhello_ascii:offset:4}")
+ if [[ $extension_len -lt $len1+4 ]] || [[ $len1 -lt 4 ]]; then
+ debugme tmln_warning "Malformed supported groups extension."
+ return 1
+ fi
+ offset=$((offset+4))
+ for (( j=0; j < len1; j=j+4 )); do
+ [[ $j -ne 0 ]] && echo -n ", " >> $TMPFILE
+ case "${tls_serverhello_ascii:offset:4}" in
+ "0017") echo -n "secp256r1" >> $TMPFILE ;;
+ "0018") echo -n "secp384r1" >> $TMPFILE ;;
+ "0019") echo -n "secp521r1" >> $TMPFILE ;;
+ "001D") echo -n "X25519" >> $TMPFILE ;;
+ "001E") echo -n "X448" >> $TMPFILE ;;
+ "0100") echo -n "ffdhe2048" >> $TMPFILE ;;
+ "0101") echo -n "ffdhe3072" >> $TMPFILE ;;
+ "0102") echo -n "ffdhe4096" >> $TMPFILE ;;
+ "0103") echo -n "ffdhe6144" >> $TMPFILE ;;
+ "0104") echo -n "ffdhe8192" >> $TMPFILE ;;
+ *) echo -n "unknown (${tls_serverhello_ascii:offset:4})" >> $TMPFILE ;;
+ esac
+ offset=$((offset+4))
+ done
+ echo "" >> $TMPFILE
+ fi
+ ;;
+ 000B) tls_extensions+="TLS server extension \"EC point formats\" (id=11), len=$extension_len\n" ;;
+ 000C) tls_extensions+="TLS server extension \"SRP\" (id=12), len=$extension_len\n" ;;
+ 000D) tls_extensions+="TLS server extension \"signature algorithms\" (id=13), len=$extension_len\n" ;;
+ 000E) tls_extensions+="TLS server extension \"use SRTP\" (id=14), len=$extension_len\n" ;;
+ 000F) tls_extensions+="TLS server extension \"heartbeat\" (id=15), len=$extension_len\n" ;;
+ 0010) tls_extensions+="TLS server extension \"application layer protocol negotiation\" (id=16), len=$extension_len\n"
+ if [[ "$process_full" =~ all ]]; then
+ if [[ $extension_len -lt 4 ]]; then
+ debugme echo "Malformed application layer protocol negotiation extension."
+ [[ $DEBUG -ge 1 ]] && tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+ echo -n "ALPN protocol: " >> $TMPFILE
+ offset=$((extns_offset+12+i))
+ j=2*$(hex2dec "${tls_serverhello_ascii:offset:4}")
+ if [[ $extension_len -ne $j+4 ]] || [[ $j -lt 2 ]]; then
+ debugme echo "Malformed application layer protocol negotiation extension."
+ [[ $DEBUG -ge 1 ]] && tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+ offset=$((offset+4))
+ j=2*$(hex2dec "${tls_serverhello_ascii:offset:2}")
+ if [[ $extension_len -ne $j+6 ]]; then
+ debugme echo "Malformed application layer protocol negotiation extension."
+ [[ $DEBUG -ge 1 ]] && tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+ offset=$((offset+2))
+ asciihex_to_binary "${tls_serverhello_ascii:offset:j}" >> "$TMPFILE"
+ echo "" >> $TMPFILE
+ echo "===============================================================================" >> $TMPFILE
+ fi
+ ;;
+ 0011) tls_extensions+="TLS server extension \"certificate status version 2\" (id=17), len=$extension_len\n" ;;
+ 0012) tls_extensions+="TLS server extension \"signed certificate timestamps\" (id=18), len=$extension_len\n" ;;
+ 0013) tls_extensions+="TLS server extension \"client certificate type\" (id=19), len=$extension_len\n" ;;
+ 0014) tls_extensions+="TLS server extension \"server certificate type\" (id=20), len=$extension_len\n" ;;
+ 0015) tls_extensions+="TLS server extension \"TLS padding\" (id=21), len=$extension_len\n" ;;
+ 0016) tls_extensions+="TLS server extension \"encrypt-then-mac\" (id=22), len=$extension_len\n" ;;
+ 0017) tls_extensions+="TLS server extension \"extended master secret\" (id=23), len=$extension_len\n" ;;
+ 0018) tls_extensions+="TLS server extension \"token binding\" (id=24), len=$extension_len\n" ;;
+ 0019) tls_extensions+="TLS server extension \"cached info\" (id=25), len=$extension_len\n" ;;
+ 0023) tls_extensions+="TLS server extension \"session ticket\" (id=35), len=$extension_len\n" ;;
+ 0028|0033)
+ # The key share extension was renumbered from 40 to 51 in TLSv1.3 draft 23 since a few
+ # implementations have been using 40 for the extended_random extension. Since the
+ # server's version may not yet have been determined, assume that both values represent the
+ # key share extension.
+ if [[ "$extension_type" == "00$KEY_SHARE_EXTN_NR" ]]; then
+ tls_extensions+="TLS server extension \"key share\""
+ else
+ tls_extensions+="TLS server extension \"unrecognized extension\""
+ fi
+ if [[ "$extension_type" == 0028 ]]; then
+ tls_extensions+=" (id=40), len=$extension_len\n"
+ else
+ tls_extensions+=" (id=51), len=$extension_len\n"
+ fi
+ if [[ "$process_full" =~ all ]] || [[ "$process_full" == ephemeralkey ]]; then
+ if [[ $extension_len -lt 4 ]]; then
+ debugme tmln_warning "Malformed key share extension."
+ [[ $DEBUG -ge 1 ]] && tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+ offset=$((extns_offset+12+i))
+ named_curve=$(hex2dec "${tls_serverhello_ascii:offset:4}")
+ offset=$((extns_offset+16+i))
+ msg_len=2*"$(hex2dec "${tls_serverhello_ascii:offset:4}")"
+ if [[ $msg_len -ne $extension_len-8 ]]; then
+ debugme tmln_warning "Malformed key share extension."
+ [[ $DEBUG -ge 1 ]] && tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+ case $named_curve in
+ 21) dh_bits=224 ; named_curve_str="P-224" ; named_curve_oid="06052b81040021" ;;
+ 23) dh_bits=256 ; named_curve_str="P-256" ; named_curve_oid="06082a8648ce3d030107" ;;
+ 24) dh_bits=384 ; named_curve_str="P-384" ; named_curve_oid="06052b81040022" ;;
+ 25) dh_bits=521 ; named_curve_str="P-521" ; named_curve_oid="06052b81040023" ;;
+ 29) dh_bits=253 ; named_curve_str="X25519" ;;
+ 30) dh_bits=448 ; named_curve_str="X448" ;;
+ 256) dh_bits=2048 ; named_curve_str="ffdhe2048" ;;
+ 257) dh_bits=3072 ; named_curve_str="ffdhe3072" ;;
+ 258) dh_bits=4096 ; named_curve_str="ffdhe4096" ;;
+ 259) dh_bits=6144 ; named_curve_str="ffdhe6144" ;;
+ 260) dh_bits=8192 ; named_curve_str="ffdhe8192" ;;
+ *) named_curve_str="" ; named_curve_oid="" ;;
+ esac
+ offset=$((extns_offset+20+i))
+ if ! "$HAS_PKEY"; then
+ # The key can't be extracted without the pkey utility.
+ key_bitstring=""
+ elif [[ $named_curve -eq 29 ]]; then
+ key_bitstring="302a300506032b656e032100${tls_serverhello_ascii:offset:msg_len}"
+ elif [[ $named_curve -eq 30 ]]; then
+ key_bitstring="3042300506032b656f033900${tls_serverhello_ascii:offset:msg_len}"
+ elif [[ $named_curve -lt 256 ]] && [[ -n "$named_curve_oid" ]]; then
+ len1="$(printf "%02x" $((msg_len/2+1)))"
+ [[ "0x${len1}" -ge "0x80" ]] && len1="81${len1}"
+ key_bitstring="03${len1}00${tls_serverhello_ascii:offset:msg_len}"
+ len2="$(printf "%02x" $((${#named_curve_oid}/2+9)))"
+ len3="$(printf "%02x" $((${#named_curve_oid}/2+${#key_bitstring}/2+11)))"
+ [[ "0x${len3}" -ge "0x80" ]] && len3="81${len3}"
+ key_bitstring="30${len3}30${len2}06072a8648ce3d0201${named_curve_oid}${key_bitstring}"
+ elif [[ "$named_curve_str" =~ "ffdhe" ]] && [[ "${TLS13_KEY_SHARES[named_curve]}" =~ "BEGIN" ]]; then
+ dh_param="$($OPENSSL pkey -pubout -outform DER 2>>$ERRFILE <<< "${TLS13_KEY_SHARES[named_curve]}" | hexdump -v -e '16/1 "%02X"')"
+
+ # First is the length of the public-key SEQUENCE, and it is always encoded in four bytes (3082xxxx)
+ # Next is the length of the parameters SEQUENCE, and it is also always encoded in four bytes (3082xxxx)
+ dh_param_len=8+2*"$(hex2dec "${dh_param:12:4}")"
+ dh_param="${dh_param:8:dh_param_len}"
+ if [[ "0x${tls_serverhello_ascii:offset:2}" -ge 0x80 ]]; then
+ key_bitstring="00${tls_serverhello_ascii:offset:msg_len}"
+ msg_len+=2
+ else
+ key_bitstring="${tls_serverhello_ascii:offset:msg_len}"
+ fi
+ len1="$(printf "%04x" $((msg_len/2)))"
+ key_bitstring="0282${len1}$key_bitstring"
+ len1="$(printf "%04x" $((${#key_bitstring}/2+1)))"
+ key_bitstring="${dh_param}0382${len1}00$key_bitstring"
+ len1="$(printf "%04x" $((${#key_bitstring}/2)))"
+ key_bitstring="3082${len1}$key_bitstring"
+ fi
+ if [[ -n "$key_bitstring" ]]; then
+ key_bitstring="$(asciihex_to_binary "$key_bitstring" | $OPENSSL pkey -pubin -inform DER 2>$ERRFILE)"
+ if [[ -z "$key_bitstring" ]] && [[ $DEBUG -ge 2 ]]; then
+ if [[ -n "$named_curve_str" ]]; then
+ prln_warning "Your $OPENSSL doesn't support $named_curve_str"
+ else
+ prln_warning "Your $OPENSSL doesn't support named curve $named_curve"
+ fi
+ fi
+ fi
+ fi
+ ;;
+ 0029) tls_extensions+="TLS server extension \"pre-shared key\" (id=41), len=$extension_len\n" ;;
+ 002A) tls_extensions+="TLS server extension \"early data\" (id=42), len=$extension_len\n" ;;
+ 002B) tls_extensions+="TLS server extension \"supported versions\" (id=43), len=$extension_len\n"
+ if [[ $extension_len -ne 4 ]]; then
+ debugme tmln_warning "Malformed supported versions extension."
+ return 1
+ fi
+ offset=$((extns_offset+12+i))
+ tls_protocol2="${tls_serverhello_ascii:offset:4}"
+ DETECTED_TLS_VERSION="$tls_protocol2"
+ [[ "${DETECTED_TLS_VERSION:0:2}" == 7F ]] && DETECTED_TLS_VERSION="0304"
+ ;;
+ 002C) tls_extensions+="TLS server extension \"cookie\" (id=44), len=$extension_len\n" ;;
+ 002D) tls_extensions+="TLS server extension \"psk key exchange modes\" (id=45), len=$extension_len\n" ;;
+ 002E) tls_extensions+="TLS server extension \"ticket early data info\" (id=46), len=$extension_len\n" ;;
+ 002F) tls_extensions+="TLS server extension \"certificate authorities\" (id=47), len=$extension_len\n" ;;
+ 0030) tls_extensions+="TLS server extension \"oid filters\" (id=48), len=$extension_len\n" ;;
+ 0031) tls_extensions+="TLS server extension \"post handshake auth\" (id=49), len=$extension_len\n" ;;
+ 3374) tls_extensions+="TLS server extension \"next protocol\" (id=13172), len=$extension_len\n"
+ if [[ "$process_full" =~ all ]]; then
+ local -i protocol_len
+ echo -n "Protocols advertised by server: " >> $TMPFILE
+ offset=$((extns_offset+12+i))
+ for (( j=0; j<extension_len; j=j+protocol_len+2 )); do
+ if [[ $extension_len -lt $j+2 ]]; then
+ debugme echo "Malformed next protocol extension."
+ [[ $DEBUG -ge 1 ]] && tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+ protocol_len=2*$(hex2dec "${tls_serverhello_ascii:offset:2}")
+ if [[ $extension_len -lt $j+$protocol_len+2 ]]; then
+ debugme echo "Malformed next protocol extension."
+ [[ $DEBUG -ge 1 ]] && tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+ offset=$((offset+2))
+ asciihex_to_binary "${tls_serverhello_ascii:offset:protocol_len}" >> "$TMPFILE"
+ offset=$((offset+protocol_len))
+ [[ $j+$protocol_len+2 -lt $extension_len ]] && echo -n ", " >> $TMPFILE
+ done
+ echo "" >> $TMPFILE
+ echo "===============================================================================" >> $TMPFILE
+ fi
+ ;;
+ FF01) tls_extensions+="TLS server extension \"renegotiation info\" (id=65281), len=$extension_len\n" ;;
+ *) tls_extensions+="TLS server extension \"unrecognized extension\" (id=$(printf "%d\n\n" "0x$extension_type")), len=$extension_len\n" ;;
+ esac
+ # After processing all of the extensions in the ServerHello message,
+ # if it has been determined that the response is TLSv1.3 and the
+ # response was decrypted, then modify $tls_serverhello_ascii by adding
+ # the extensions from the EncryptedExtensions and Certificate messages
+ # and then process them.
+ if ! "$added_encrypted_extensions" && [[ "$DETECTED_TLS_VERSION" == "0304" ]] && \
+ [[ $((i+8+extension_len)) -eq $tls_extensions_len ]]; then
+ # Note that the encrypted extensions have been added so that
+ # the aren't added a second time.
+ added_encrypted_extensions=true
+ if [[ -n "$tls_encryptedextensions_ascii" ]]; then
+ tls_serverhello_ascii_len+=$tls_encryptedextensions_ascii_len-4
+ tls_extensions_len+=$tls_encryptedextensions_ascii_len-4
+ tls_encryptedextensions_ascii_len=$tls_encryptedextensions_ascii_len/2-2
+ offset=$((extns_offset+4))
+ tls_serverhello_ascii="${tls_serverhello_ascii:0:extns_offset}$(printf "%04X" $((0x${tls_serverhello_ascii:extns_offset:4}+$tls_encryptedextensions_ascii_len)))${tls_serverhello_ascii:offset}${tls_encryptedextensions_ascii:4}"
+ fi
+ if [[ -n "$tls_certificate_ascii" ]]; then
+ # In TLS 1.3, the Certificate message begins with a zero length certificate_request_context.
+ # In addition, certificate_list is now a list of (certificate, extension) pairs rather than
+ # just certificates. So, extract the extensions and add them to $tls_serverhello_ascii and
+ # create a new $tls_certificate_ascii that only contains a list of certificates.
+ if [[ -n "$tls_certificate_ascii" ]]; then
+ if [[ "${tls_certificate_ascii:0:2}" != "00" ]]; then
+ debugme tmln_warning "Malformed Certificate Handshake message in ServerHello."
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+ if [[ $tls_certificate_ascii_len -lt 8 ]]; then
+ debugme tmln_warning "Malformed Certificate Handshake message in ServerHello."
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+ certificate_list_len=2*$(hex2dec "${tls_certificate_ascii:2:6}")
+ if [[ $certificate_list_len -ne $tls_certificate_ascii_len-8 ]]; then
+ debugme tmln_warning "Malformed Certificate Handshake message in ServerHello."
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+ for (( j=8; j < tls_certificate_ascii_len; j=j+extn_len )); do
+ if [[ $tls_certificate_ascii_len-$j -lt 6 ]]; then
+ debugme tmln_warning "Malformed Certificate Handshake message in ServerHello."
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+ certificate_len=2*$(hex2dec "${tls_certificate_ascii:j:6}")
+ if [[ $certificate_len -gt $tls_certificate_ascii_len-$j-6 ]]; then
+ debugme tmln_warning "Malformed Certificate Handshake message in ServerHello."
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+ len1=$certificate_len+6
+ tls_revised_certificate_msg+="${tls_certificate_ascii:j:len1}"
+ j+=$len1
+ extn_len=2*$(hex2dec "${tls_certificate_ascii:j:4}")
+ j+=4
+ # TODO: Should only the extensions associated with the EE certificate be added to $tls_serverhello_ascii?
+ tls_serverhello_ascii_len+=$extn_len
+ tls_extensions_len+=$extn_len
+ offset=$((extns_offset+4))
+ tls_serverhello_ascii="${tls_serverhello_ascii:0:extns_offset}$(printf "%04X" $(( 0x${tls_serverhello_ascii:extns_offset:4}+extn_len/2)) )${tls_serverhello_ascii:offset}${tls_certificate_ascii:j:extn_len}"
+ done
+ tls_certificate_ascii_len=${#tls_revised_certificate_msg}+6
+ tls_certificate_ascii="$(printf "%06X" $(( tls_certificate_ascii_len/2-3)) )$tls_revised_certificate_msg"
+ fi
+ fi
+ fi
+ done
+ fi
+
+ if [[ "$DETECTED_TLS_VERSION" == "0300" ]]; then
+ echo "Protocol : SSLv3" >> $TMPFILE
+ else
+ echo "Protocol : TLSv1.$((0x$DETECTED_TLS_VERSION-0x0301))" >> $TMPFILE
+ fi
+ echo "===============================================================================" >> $TMPFILE
+ if [[ $TLS_NR_CIPHERS -ne 0 ]]; then
+ if [[ "${tls_cipher_suite:0:2}" == "00" ]]; then
+ rfc_cipher_suite="$(show_rfc_style "x${tls_cipher_suite:2:2}")"
+ else
+ rfc_cipher_suite="$(show_rfc_style "x${tls_cipher_suite:0:4}")"
+ fi
+ elif "$HAS_CIPHERSUITES"; then
+ rfc_cipher_suite="$($OPENSSL ciphers -V -ciphersuites "$TLS13_OSSL_CIPHERS" 'ALL:COMPLEMENTOFALL' | grep -i " 0x${tls_cipher_suite:0:2},0x${tls_cipher_suite:2:2} " | awk '{ print $3 }')"
+ else
+ rfc_cipher_suite="$($OPENSSL ciphers -V 'ALL:COMPLEMENTOFALL' | grep -i " 0x${tls_cipher_suite:0:2},0x${tls_cipher_suite:2:2} " | awk '{ print $3 }')"
+ fi
+ echo "Cipher : $rfc_cipher_suite" >> $TMPFILE
+ if [[ $dh_bits -ne 0 ]]; then
+ if [[ "$named_curve_str" =~ "ffdhe" ]]; then
+ echo "Server Temp Key: DH, $named_curve_str, $dh_bits bits" >> $TMPFILE
+ elif [[ "$named_curve_str" == "X25519" ]] || [[ "$named_curve_str" == "X448" ]]; then
+ echo "Server Temp Key: $named_curve_str, $dh_bits bits" >> $TMPFILE
+ else
+ echo "Server Temp Key: ECDH, $named_curve_str, $dh_bits bits" >> $TMPFILE
+ fi
+ fi
+ if [[ -n "$key_bitstring" ]]; then
+ echo "$key_bitstring" >> $TMPFILE
+ [[ "${TLS13_KEY_SHARES[named_curve]}" =~ "BEGIN" ]] && \
+ echo "${TLS13_KEY_SHARES[named_curve]}" >> $TMPFILE
+ fi
+ echo "===============================================================================" >> $TMPFILE
+ if [[ "0x${DETECTED_TLS_VERSION:2:2}" -le "0x03" ]]; then
+ case $tls_compression_method in
+ 00) echo "Compression: NONE" >> $TMPFILE ;;
+ 01) echo "Compression: zlib compression" >> $TMPFILE ;;
+ 40) echo "Compression: LZS compression" >> $TMPFILE ;;
+ *) echo "Compression: unrecognized compression method" >> $TMPFILE ;;
+ esac
+ echo "===============================================================================" >> $TMPFILE
+ fi
+ [[ -n "$tls_extensions" ]] && echo -e "$tls_extensions" >> $TMPFILE
+
+ if [[ $DEBUG -ge 3 ]]; then
+ echo "TLS server hello message:"
+ if [[ $DEBUG -ge 4 ]]; then
+ echo " tls_protocol: 0x$tls_protocol2"
+ [[ "0x${DETECTED_TLS_VERSION:2:2}" -le "0x03" ]] && echo " tls_sid_len: 0x$tls_sid_len_hex / = $((tls_sid_len/2))"
+ fi
+ if [[ "0x${DETECTED_TLS_VERSION:2:2}" -le "0x03" ]]; then
+ echo -n " tls_hello_time: 0x$tls_hello_time "
+ parse_date "$TLS_TIME" "+%Y-%m-%d %r" "%s" # in debugging mode we don't mind the cycles and don't use TLS_DIFFTIME_SET
+ fi
+ echo -n " tls_cipher_suite: 0x$tls_cipher_suite"
+ if [[ -n "$rfc_cipher_suite" ]]; then
+ echo " ($rfc_cipher_suite)"
+ else
+ echo ""
+ fi
+ if [[ $dh_bits -ne 0 ]]; then
+ if [[ "$named_curve_str" =~ "ffdhe" ]]; then
+ echo " dh_bits: DH, $named_curve_str, $dh_bits bits"
+ elif [[ "$named_curve_str" == "X25519" ]] || [[ "$named_curve_str" == "X448" ]]; then
+ echo " dh_bits: $named_curve_str, $dh_bits bits"
+ else
+ echo " dh_bits: ECDH, $named_curve_str, $dh_bits bits"
+ fi
+ fi
+ if [[ "0x${DETECTED_TLS_VERSION:2:2}" -le "0x03" ]]; then
+ echo -n " tls_compression_method: 0x$tls_compression_method "
+ case $tls_compression_method in
+ 00) echo "(NONE)" ;;
+ 01) echo "(zlib compression)" ;;
+ 40) echo "(LZS compression)" ;;
+ *) echo "(unrecognized compression method)" ;;
+ esac
+ fi
+ if [[ -n "$tls_extensions" ]]; then
+ echo -n " tls_extensions: "
+ newline_to_spaces "$(grep -a 'TLS server extension ' $TMPFILE | \
+ sed -e 's/TLS server extension //g' -e 's/\" (id=/\/#/g' \
+ -e 's/,.*$/,/g' -e 's/),$/\"/g' \
+ -e 's/elliptic curves\/#10/supported_groups\/#10/g')"
+ echo ""
+ if [[ "$tls_extensions" =~ supported_groups ]]; then
+ echo " Supported Groups: $(grep "Supported groups:" "$TMPFILE" | sed 's/Supported groups: //')"
+ fi
+ if [[ "$tls_extensions" =~ application\ layer\ protocol\ negotiation ]]; then
+ echo " ALPN protocol: $(grep "ALPN protocol:" "$TMPFILE" | sed 's/ALPN protocol: //')"
+ fi
+ if [[ "$tls_extensions" =~ next\ protocol ]]; then
+ echo " NPN protocols: $(grep "Protocols advertised by server:" "$TMPFILE" | sed 's/Protocols advertised by server: //')"
+ fi
+ fi
+ tmln_out
+ fi
+
+ # If a CIPHER_SUITES string was provided, then check that $tls_cipher_suite is in the string.
+ # this appeared in yassl + MySQL (https://github.com/drwetter/testssl.sh/pull/784) but adds robustness
+ # to the implementation
+ if [[ -n "$cipherlist" ]]; then
+ tls_cipher_suite="$(tolower "$tls_cipher_suite")"
+ tls_cipher_suite="${tls_cipher_suite:0:2}\\x${tls_cipher_suite:2:2}"
+ cipherlist_len=${#cipherlist}
+ for (( i=0; i < cipherlist_len; i=i+8 )); do
+ # At the right hand side we need the quotes here!
+ [[ "${cipherlist:i:6}" == "$tls_cipher_suite" ]] && break
+ done
+ if [[ $i -ge $cipherlist_len ]]; then
+ BAD_SERVER_HELLO_CIPHER=true
+ debugme echo "The ServerHello specifies a cipher suite that wasn't included in the ClientHello."
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+ fi
+
+ # If the ClientHello included a supported_versions extension, then check that the
+ # $DETECTED_TLS_VERSION appeared in the list offered in the ClientHello.
+ if [[ "${TLS_CLIENT_HELLO:0:2}" == 01 ]]; then
+ # get position of cipher lists (just after session id)
+ offset=78+2*$(hex2dec "${TLS_CLIENT_HELLO:76:2}")
+ # get position of compression methods
+ offset+=4+2*$(hex2dec "${TLS_CLIENT_HELLO:offset:4}")
+ # get position of extensions
+ extns_offset=$offset+6+2*$(hex2dec "${TLS_CLIENT_HELLO:offset:2}")
+ len1=${#TLS_CLIENT_HELLO}
+ for (( i=extns_offset; i < len1; i=i+8+extension_len )); do
+ extension_type="${TLS_CLIENT_HELLO:i:4}"
+ offset=4+$i
+ extension_len=2*$(hex2dec "${TLS_CLIENT_HELLO:offset:4}")
+ if [[ "$extension_type" == 002b ]]; then
+ offset+=6
+ tls_protocol2="$(tolower "$tls_protocol2")"
+ for (( j=0; j < extension_len-2; j=j+4 )); do
+ [[ "${TLS_CLIENT_HELLO:offset:4}" == $tls_protocol2 ]] && break
+ offset+=4
+ done
+ if [[ $j -eq $extension_len-2 ]]; then
+ debugme echo "The ServerHello specifies a version that wasn't offered in the ClientHello."
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+ break
+ fi
+ done
+ fi
+
+ # Now parse the Certificate message.
+ if [[ "$process_full" =~ all ]]; then
+ # not sure why we need this
+ [[ -e "$HOSTCERT" ]] && rm "$HOSTCERT"
+ [[ -e "$TEMPDIR/intermediatecerts.pem" ]] && > "$TEMPDIR/intermediatecerts.pem"
+ fi
+ if [[ $tls_certificate_ascii_len -ne 0 ]]; then
+ # The first certificate is the server's certificate. If there are anything
+ # subsequent certificates, they are intermediate certificates.
+ if [[ $tls_certificate_ascii_len -lt 12 ]]; then
+ debugme echo "Malformed Certificate Handshake message in ServerHello."
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+ certificate_list_len=2*$(hex2dec "${tls_certificate_ascii:0:6}")
+ if [[ $certificate_list_len -ne $tls_certificate_ascii_len-6 ]]; then
+ debugme echo "Malformed Certificate Handshake message in ServerHello."
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+
+ # Place server's certificate in $HOSTCERT
+ certificate_len=2*$(hex2dec "${tls_certificate_ascii:6:6}")
+ if [[ $certificate_len -gt $tls_certificate_ascii_len-12 ]]; then
+ debugme echo "Malformed Certificate Handshake message in ServerHello."
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+ asciihex_to_binary "${tls_certificate_ascii:12:certificate_len}" | \
+ $OPENSSL x509 -inform DER -outform PEM -out "$HOSTCERT" 2>$ERRFILE
+ if [[ $? -ne 0 ]]; then
+ debugme echo "Malformed certificate in Certificate Handshake message in ServerHello."
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+ get_pub_key_size
+ echo "===============================================================================" >> $TMPFILE
+ echo "---" >> $TMPFILE
+ echo "Certificate chain" >> $TMPFILE
+ subjectDN="$($OPENSSL x509 -in $HOSTCERT -noout -subject 2>>$ERRFILE)"
+ issuerDN="$($OPENSSL x509 -in $HOSTCERT -noout -issuer 2>>$ERRFILE)"
+ echo " $nr_certs s:${subjectDN:9}" >> $TMPFILE
+ echo " i:${issuerDN:8}" >> $TMPFILE
+ cat "$HOSTCERT" >> $TMPFILE
+
+ echo "" > "$TEMPDIR/intermediatecerts.pem"
+ # Place any additional certificates in $TEMPDIR/intermediatecerts.pem
+ CERTIFICATE_LIST_ORDERING_PROBLEM=false
+ CAissuerDN="$issuerDN"
+ for (( i=12+certificate_len; i<tls_certificate_ascii_len; i=i+certificate_len )); do
+ if [[ $tls_certificate_ascii_len-$i -lt 6 ]]; then
+ debugme echo "Malformed Certificate Handshake message in ServerHello."
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+ certificate_len=2*$(hex2dec "${tls_certificate_ascii:i:6}")
+ i+=6
+ if [[ $certificate_len -gt $tls_certificate_ascii_len-$i ]]; then
+ debugme echo "Malformed certificate in Certificate Handshake message in ServerHello."
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+ pem_certificate="$(asciihex_to_binary "${tls_certificate_ascii:i:certificate_len}" | \
+ $OPENSSL x509 -inform DER -outform PEM 2>$ERRFILE)"
+ if [[ $? -ne 0 ]]; then
+ debugme echo "Malformed certificate in Certificate Handshake message in ServerHello."
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+ nr_certs+=1
+ CAsubjectDN="$($OPENSSL x509 -noout -subject 2>>$ERRFILE <<< "$pem_certificate")"
+ # Check that this certificate certifies the one immediately preceding it.
+ [[ "${CAsubjectDN:9}" != "${CAissuerDN:8}" ]] && CERTIFICATE_LIST_ORDERING_PROBLEM=true
+ CAissuerDN="$($OPENSSL x509 -noout -issuer 2>>$ERRFILE <<< "$pem_certificate")"
+ echo " $nr_certs s:${CAsubjectDN:9}" >> $TMPFILE
+ echo " i:${CAissuerDN:8}" >> $TMPFILE
+ echo "$pem_certificate" >> $TMPFILE
+ echo "$pem_certificate" >> "$TEMPDIR/intermediatecerts.pem"
+ if [[ -z "$hostcert_issuer" ]] && [[ "${CAsubjectDN:9}" == "${issuerDN:8}" ]]; then
+ # The issuer's certificate is needed if there is a stapled OCSP response,
+ # and it may be needed if check_revocation_ocsp() will later be called
+ # with the OCSP URI in the server's certificate.
+ hostcert_issuer="$TEMPDIR/hostcert_issuer.pem"
+ echo "$pem_certificate" > "$hostcert_issuer"
+ fi
+ done
+ echo "---" >> $TMPFILE
+ echo "Server certificate" >> $TMPFILE
+ echo "subject=${subjectDN:9}" >> $TMPFILE
+ echo "issuer=${issuerDN:8}" >> $TMPFILE
+ echo "---" >> $TMPFILE
+ fi
+
+ # Now parse the certificate status message
+ if [[ $tls_certificate_status_ascii_len -ne 0 ]] && [[ $tls_certificate_status_ascii_len -lt 8 ]]; then
+ debugme echo "Malformed certificate status Handshake message in ServerHello."
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ elif [[ $tls_certificate_status_ascii_len -ne 0 ]] && [[ "${tls_certificate_status_ascii:0:2}" == "01" ]]; then
+ # This is a certificate status message of type "ocsp"
+ ocsp_response_len=2*$(hex2dec "${tls_certificate_status_ascii:2:6}")
+ if [[ $ocsp_response_len -ne $tls_certificate_status_ascii_len-8 ]]; then
+ debugme echo "Malformed certificate status Handshake message in ServerHello."
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+ ocsp_resp_offset=8
+ elif [[ $tls_certificate_status_ascii_len -ne 0 ]] && [[ "${tls_certificate_status_ascii:0:2}" == "02" ]]; then
+ # This is a list of OCSP responses, but only the first one is needed
+ # since the first one corresponds to the server's certificate.
+ ocsp_response_list_len=2*$(hex2dec "${tls_certificate_status_ascii:2:6}")
+ if [[ $ocsp_response_list_len -ne $tls_certificate_status_ascii_len-8 ]] || [[ $ocsp_response_list_len -lt 6 ]]; then
+ debugme echo "Malformed certificate status Handshake message in ServerHello."
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+ ocsp_response_len=2*$(hex2dec "${tls_certificate_status_ascii:8:6}")
+ if [[ $ocsp_response_len -gt $ocsp_response_list_len-6 ]]; then
+ debugme echo "Malformed certificate status Handshake message in ServerHello."
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+ ocsp_resp_offset=14
+ fi
+ STAPLED_OCSP_RESPONSE=""
+ if [[ $ocsp_response_len -ne 0 ]]; then
+ STAPLED_OCSP_RESPONSE="${tls_certificate_status_ascii:ocsp_resp_offset:ocsp_response_len}"
+ echo "OCSP response:" >> $TMPFILE
+ echo "===============================================================================" >> $TMPFILE
+ if [[ -n "$hostcert_issuer" ]]; then
+ asciihex_to_binary "$STAPLED_OCSP_RESPONSE" | \
+ $OPENSSL ocsp -no_nonce -CAfile $TEMPDIR/intermediatecerts.pem -issuer $hostcert_issuer -cert $HOSTCERT -respin /dev/stdin -resp_text >> $TMPFILE 2>$ERRFILE
+ else
+ asciihex_to_binary "$STAPLED_OCSP_RESPONSE" | \
+ $OPENSSL ocsp -respin /dev/stdin -resp_text >> $TMPFILE 2>$ERRFILE
+ fi
+ echo "===============================================================================" >> $TMPFILE
+ elif [[ "$process_full" =~ all ]]; then
+ echo "OCSP response: no response sent" >> $TMPFILE
+ echo "===============================================================================" >> $TMPFILE
+ fi
+
+ # Now parse the server key exchange message
+ if [[ $tls_serverkeyexchange_ascii_len -ne 0 ]]; then
+ if [[ $rfc_cipher_suite =~ TLS_ECDHE_ ]] || [[ $rfc_cipher_suite =~ TLS_ECDH_anon ]] || \
+ [[ $rfc_cipher_suite == ECDHE* ]] || [[ $rfc_cipher_suite == AECDH* ]]; then
+ if [[ $tls_serverkeyexchange_ascii_len -lt 6 ]]; then
+ debugme echo "Malformed ServerKeyExchange Handshake message in ServerHello."
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+ curve_type=$(hex2dec "${tls_serverkeyexchange_ascii:0:2}")
+ if [[ $curve_type -eq 3 ]]; then
+ # named_curve - the curve is identified by a 2-byte number
+ named_curve=$(hex2dec "${tls_serverkeyexchange_ascii:2:4}")
+ # https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-8
+ case $named_curve in
+ 1) dh_bits=163 ; named_curve_str="K-163" ;;
+ 2) dh_bits=162 ; named_curve_str="sect163r1" ;;
+ 3) dh_bits=163 ; named_curve_str="B-163" ;;
+ 4) dh_bits=193 ; named_curve_str="sect193r1" ;;
+ 5) dh_bits=193 ; named_curve_str="sect193r2" ;;
+ 6) dh_bits=232 ; named_curve_str="K-233" ;;
+ 7) dh_bits=233 ; named_curve_str="B-233" ;;
+ 8) dh_bits=238 ; named_curve_str="sect239k1" ;;
+ 9) dh_bits=281 ; named_curve_str="K-283" ;;
+ 10) dh_bits=282 ; named_curve_str="B-283" ;;
+ 11) dh_bits=407 ; named_curve_str="K-409" ;;
+ 12) dh_bits=409 ; named_curve_str="B-409" ;;
+ 13) dh_bits=570 ; named_curve_str="K-571" ;;
+ 14) dh_bits=570 ; named_curve_str="B-571" ;;
+ 15) dh_bits=161 ; named_curve_str="secp160k1" ;;
+ 16) dh_bits=161 ; named_curve_str="secp160r1" ;;
+ 17) dh_bits=161 ; named_curve_str="secp160r2" ;;
+ 18) dh_bits=192 ; named_curve_str="secp192k1" ;;
+ 19) dh_bits=192 ; named_curve_str="P-192" ;;
+ 20) dh_bits=225 ; named_curve_str="secp224k1" ;;
+ 21) dh_bits=224 ; named_curve_str="P-224" ;;
+ 22) dh_bits=256 ; named_curve_str="secp256k1" ;;
+ 23) dh_bits=256 ; named_curve_str="P-256" ;;
+ 24) dh_bits=384 ; named_curve_str="P-384" ;;
+ 25) dh_bits=521 ; named_curve_str="P-521" ;;
+ 26) dh_bits=256 ; named_curve_str="brainpoolP256r1" ;;
+ 27) dh_bits=384 ; named_curve_str="brainpoolP384r1" ;;
+ 28) dh_bits=512 ; named_curve_str="brainpoolP512r1" ;;
+ 29) dh_bits=253 ; named_curve_str="X25519" ;;
+ 30) dh_bits=448 ; named_curve_str="X448" ;;
+ esac
+ fi
+ if [[ $dh_bits -ne 0 ]] && [[ $named_curve -ne 29 ]] && [[ $named_curve -ne 30 ]]; then
+ [[ $DEBUG -ge 3 ]] && echo -e " dh_bits: ECDH, $named_curve_str, $dh_bits bits\n"
+ echo "Server Temp Key: ECDH, $named_curve_str, $dh_bits bits" >> $TMPFILE
+ elif [[ $dh_bits -ne 0 ]]; then
+ [[ $DEBUG -ge 3 ]] && echo -e " dh_bits: $named_curve_str, $dh_bits bits\n"
+ echo "Server Temp Key: $named_curve_str, $dh_bits bits" >> $TMPFILE
+ fi
+ elif [[ $rfc_cipher_suite =~ TLS_DHE_ ]] || [[ $rfc_cipher_suite =~ TLS_DH_anon ]] || \
+ [[ $rfc_cipher_suite == "DHE-"* ]] || [[ $rfc_cipher_suite == "EDH-"* ]] || \
+ [[ $rfc_cipher_suite == "EXP1024-DHE-"* ]]; then
+ # For DH ephemeral keys the first field is p, and the length of
+ # p is the same as the length of the public key.
+ if [[ $tls_serverkeyexchange_ascii_len -lt 4 ]]; then
+ debugme echo "Malformed ServerKeyExchange Handshake message in ServerHello."
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+ dh_p_len=2*$(hex2dec "${tls_serverkeyexchange_ascii:0:4}")
+ offset=4+$dh_p_len
+ if [[ $tls_serverkeyexchange_ascii_len -lt $offset ]]; then
+ debugme echo "Malformed ServerKeyExchange Handshake message in ServerHello."
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+
+ # Subtract any leading 0 bytes
+ for (( i=4; i < offset; i=i+2 )); do
+ [[ "${tls_serverkeyexchange_ascii:i:2}" != "00" ]] && break
+ dh_p_len=$dh_p_len-2
+ done
+ if [[ $i -ge $offset ]]; then
+ debugme echo "Malformed ServerKeyExchange Handshake message in ServerHello."
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 1
+ fi
+ dh_p="${tls_serverkeyexchange_ascii:i:dh_p_len}"
+
+ dh_bits=4*$dh_p_len
+ msb=$(hex2dec "${tls_serverkeyexchange_ascii:i:2}")
+ for (( mask=128; msb < mask; mask/=2 )); do
+ dh_bits=$dh_bits-1
+ done
+
+ key_bitstring="$(get_dh_ephemeralkey "$tls_serverkeyexchange_ascii")"
+ [[ $? -eq 0 ]] && echo "$key_bitstring" >> $TMPFILE
+
+ # Check to see whether the ephemeral public key uses one of the groups from
+ # RFC 7919 for parameters
+ case $dh_bits in
+ 2048) named_curve=256; named_curve_str=" ffdhe2048," ;;
+ 3072) named_curve=257; named_curve_str=" ffdhe3072," ;;
+ 4096) named_curve=258; named_curve_str=" ffdhe4096," ;;
+ 6144) named_curve=259; named_curve_str=" ffdhe6144," ;;
+ 8192) named_curve=260; named_curve_str=" ffdhe8192," ;;
+ *) named_curve=0; named_curve_str="" ;;
+ esac
+ [[ -z "$key_bitstring" ]] && named_curve=0 && named_curve_str=""
+ if "$HAS_PKEY" && [[ $named_curve -ne 0 ]] && [[ "${TLS13_KEY_SHARES[named_curve]}" =~ BEGIN ]]; then
+ ephemeral_param="$($OPENSSL pkey -pubin -text_pub -noout 2>>$ERRFILE <<< "$key_bitstring")"
+ # OpenSSL 3.0.0 outputs the group name rather than the actual parameter values for some named groups.
+ if [[ "$ephemeral_param" =~ GROUP: ]]; then
+ ephemeral_param="${ephemeral_param#*GROUP: }"
+ rfc7919_param="${named_curve_str# }"
+ rfc7919_param="${rfc7919_param%,}"
+ [[ "$ephemeral_param" =~ $rfc7919_param ]] || named_curve_str=""
+ else
+ ephemeral_param="$(grep -EA 1000 "prime:|P:" <<< "$ephemeral_param")"
+ rfc7919_param="$($OPENSSL pkey -text_pub -noout 2>>$ERRFILE <<< "${TLS13_KEY_SHARES[named_curve]}" | grep -EA 1000 "prime:|P:")"
+ [[ "$ephemeral_param" != "$rfc7919_param" ]] && named_curve_str=""
+ fi
+ fi
+
+ [[ $DEBUG -ge 3 ]] && [[ $dh_bits -ne 0 ]] && echo -e " dh_bits: DH,$named_curve_str $dh_bits bits\n"
+ [[ $dh_bits -ne 0 ]] && echo "Server Temp Key: DH,$named_curve_str $dh_bits bits" >> $TMPFILE
+ fi
+ fi
+ tmpfile_handle ${FUNCNAME[0]}.txt
+
+ TLS_SERVER_HELLO="02$(printf "%06x" $(( tls_serverhello_ascii_len/2)) )${tls_serverhello_ascii}"
+ return 0
+}
+
+
+#arg1 (optional): list of ciphers suites or empty
+#arg2 (optional): "true" if full server response should be parsed.
+# return: 6: couldn't open socket, 3(!): sslv2 handshake succeeded, 0=no SSLv2
+# 1,4,6,7: see return value of parse_sslv2_serverhello()
+sslv2_sockets() {
+ local ret
+ local cipher_suites="$1"
+ local client_hello len_client_hello
+ local len_ciph_suites_byte len_ciph_suites
+ local server_hello sock_reply_file2 foo
+ local -i response_len server_hello_len
+ local parse_complete=false
+
+ # this could be empty so we use '=='
+ if [[ "$2" == true ]]; then
+ parse_complete=true
+ fi
+ if [[ -z "$cipher_suites" ]]; then
+ cipher_suites="
+ 05,00,80, # 1st cipher 9 cipher specs, only classical V2 ciphers are used here, see FIXME below
+ 03,00,80, # 2nd there are v3 in v2!!! : https://tools.ietf.org/html/rfc6101#appendix-E
+ 01,00,80, # 3rd Cipher specifications introduced in version 3.0 can be included in version 2.0 client hello messages using
+ 07,00,c0, # 4th the syntax below. [..] # V2CipherSpec (see Version 3.0 name) = { 0x00, CipherSuite }; !!!!
+ 08,00,80, # 5th
+ 06,00,40, # 6th
+ 04,00,80, # 7th
+ 02,00,80, # 8th
+ 06,01,40, # 9
+ 07,01,c0, # 10
+ FF,80,00, # 11
+ FF,80,10, # 12
+ 00,00,00" # 13
+ # FIXME: http://max.euston.net/d/tip_sslciphers.html <-- also SSLv3 ciphers
+ fi
+
+ code2network "$cipher_suites" # convert CIPHER_SUITES
+ cipher_suites="$NW_STR" # we don't have the leading \x here so string length is two byte less, see next
+ len_ciph_suites_byte=${#cipher_suites}
+
+ let "len_ciph_suites_byte += 2"
+ len_ciph_suites=$(printf "%02x\n" $(( len_ciph_suites_byte / 4 )))
+ len_client_hello=$(printf "%02x\n" $((0x$len_ciph_suites + 0x19)))
+
+ client_hello="
+ ,80,$len_client_hello # length
+ ,01 # Client Hello
+ ,00,02 # SSLv2
+ ,00,$len_ciph_suites # cipher spec length
+ ,00,00 # session ID length
+ ,00,10 # challenge length
+ ,$cipher_suites
+ ,29,22,be,b3,5a,01,8b,04,fe,5f,80,03,a0,13,eb,c4" # Challenge
+ # https://idea.popcount.org/2012-06-16-dissecting-ssl-handshake/ (client)
+
+ fd_socket 5 || return 6
+ debugme echo -n "sending client hello... "
+ socksend_clienthello "$client_hello"
+
+ sockread_serverhello 32768
+ if "$parse_complete"; then
+ if [[ -s "$SOCK_REPLY_FILE" ]]; then
+ server_hello=$(hexdump -v -e '16/1 "%02X"' "$SOCK_REPLY_FILE")
+ server_hello_len=$((2 + $(hex2dec "${server_hello:1:3}") ))
+ foo="$(wc -c "$SOCK_REPLY_FILE")"
+ response_len="${foo% *}"
+ for (( 1; response_len < server_hello_len; 1 )); do
+ sock_reply_file2=${SOCK_REPLY_FILE}.2
+ mv "$SOCK_REPLY_FILE" "$sock_reply_file2"
+
+ debugme echo -n "requesting more server hello data... "
+ socksend "" $USLEEP_SND
+ sockread_serverhello 32768
+
+ [[ ! -s "$SOCK_REPLY_FILE" ]] && break
+ cat "$SOCK_REPLY_FILE" >> "$sock_reply_file2"
+ mv "$sock_reply_file2" "$SOCK_REPLY_FILE"
+ foo="$(wc -c "$SOCK_REPLY_FILE")"
+ response_len="${foo% *}"
+ done
+ fi
+ fi
+
+ debugme echo "reading server hello... "
+ if [[ "$DEBUG" -ge 4 ]]; then
+ hexdump -C "$SOCK_REPLY_FILE" | head -6
+ tmln_out
+ fi
+
+ parse_sslv2_serverhello "$SOCK_REPLY_FILE" "$parse_complete"
+ ret=$?
+
+ close_socket
+ tmpfile_handle ${FUNCNAME[0]}.dd $SOCK_REPLY_FILE
+ return $ret
+}
+
+
+# arg1: supported groups extension
+# arg2: "all" - process full response (including Certificate and certificate_status handshake messages)
+# "ephemeralkey" - extract the server's ephemeral key (if any)
+# Given the supported groups extension, create a key_share extension that includes a key share for
+# each group listed in the supported groups extension.
+generate_key_share_extension() {
+ local supported_groups
+ local -i i len supported_groups_len group
+ local extn_len list_len
+ local key_share key_shares=""
+ local -i nr_key_shares=0
+
+ supported_groups="${1//\\x/}"
+ [[ "${supported_groups:0:4}" != "000a" ]] && return 1
+
+ supported_groups_len=${#supported_groups}
+ [[ $supported_groups_len -lt 16 ]] && return 1
+
+ len=2*$(hex2dec "${supported_groups:4:4}")
+ [[ $len+8 -ne $supported_groups_len ]] && return 1
+
+ len=2*$(hex2dec "${supported_groups:8:4}")
+ [[ $len+12 -ne $supported_groups_len ]] && return 1
+
+ for (( i=12; i<supported_groups_len; i=i+4 )); do
+ group=$(hex2dec "${supported_groups:i:4}")
+ # If the Supported groups extensions lists more than one group,
+ # then don't include the larger key shares in the extension.
+ [[ $i -gt 12 ]] && [[ $group -gt 256 ]] && continue
+
+ # Versions of OpenSSL prior to 1.1.0 cannot perform operations
+ # with X25519 keys, so don't include the X25519 key share
+ # if the server's response needs to be decrypted and an
+ # older version of OpenSSL is being used.
+ if [[ $i -gt 12 ]] && [[ $group -eq 29 ]] && [[ "$2" == all ]]; then
+ [[ "$OSSL_NAME" =~ LibreSSL ]] && continue
+ if [[ $OSSL_VER_MAJOR.$OSSL_VER_MINOR != 1.1.0* ]] && \
+ [[ $OSSL_VER_MAJOR.$OSSL_VER_MINOR != 1.1.1* ]] && \
+ [[ $OSSL_VER_MAJOR.$OSSL_VER_MINOR != 3.0.0* ]]; then
+ continue
+ fi
+ fi
+
+ # Versions of OpenSSL prior to 1.1.1 cannot perform operations
+ # with X448 keys, so don't include the X448 key share
+ # if the server's response needs to be decrypted and an
+ # older version of OpenSSL is being used.
+ if [[ $i -gt 12 ]] && [[ $group -eq 30 ]] && [[ "$2" == all ]]; then
+ [[ "$OSSL_NAME" =~ LibreSSL ]] && continue
+ if [[ $OSSL_VER_MAJOR.$OSSL_VER_MINOR != 1.1.1* ]] && \
+ [[ $OSSL_VER_MAJOR.$OSSL_VER_MINOR != 3.0.0* ]]; then
+ continue
+ fi
+ fi
+
+ # NOTE: The public keys could be extracted from the private keys
+ # (TLS13_KEY_SHARES) using $OPENSSL, but only OpenSSL 1.1.0 and newer can
+ # extract the public key from an X25519 private key, and only
+ # OpenSSL 1.1.1 can extract the public key from an X448 private key.
+ key_share="${TLS13_PUBLIC_KEY_SHARES[group]}"
+ if [[ ${#key_share} -gt 4 ]]; then
+ key_shares+=",$key_share"
+ nr_key_shares+=1
+ # Don't include more than two keys, so that the extension isn't too large.
+ [[ $nr_key_shares -ge 2 ]] && break
+ fi
+ done
+ [[ -z "$key_shares" ]] && tm_out "" && return 0
+
+ len=${#key_shares}/3
+ list_len="$(printf "%04x" "$len")"
+ len+=2
+ extn_len="$(printf "%04x" "$len")"
+ tm_out "00,$KEY_SHARE_EXTN_NR,${extn_len:0:2},${extn_len:2:2},${list_len:0:2},${list_len:2:2}$key_shares"
+ return 0
+}
+
+# ARG1: TLS version low byte (00: SSLv3, 01: TLS 1.0, 02: TLS 1.1, 03: TLS 1.2)
+# ARG2: CIPHER_SUITES string (lowercase, and in the format output by code2network())
+# ARG3: "all" - process full response (including Certificate and certificate_status handshake messages)
+# "all+" - same as "all", but do not offer any curves with TLSv1.3 that are not supported by
+# $OPENSSL, since response MUST be decrypted.
+# "ephemeralkey" - extract the server's ephemeral key (if any)
+# ARG4: (optional) additional request extensions
+# ARG5: (optional): "true" if ClientHello should advertise compression methods other than "NULL"
+# ARG6: (optional): "false" if prepare_tls_clienthello() should not open a new socket
+#
+prepare_tls_clienthello() {
+ local tls_low_byte="$1" tls_legacy_version="$1"
+ local process_full="$3"
+ local new_socket=true
+ local tls_word_reclayer="03, 01" # the first TLS version number is the record layer and always 0301
+ # -- except: SSLv3 and second ClientHello after HelloRetryRequest
+ local servername_hexstr len_servername len_servername_hex
+ local hexdump_format_str part1 part2
+ local all_extensions=""
+ local -i i j len_extension len_padding_extension len_all len_session_id
+ local len_sni_listlen len_sni_ext len_extension_hex len_padding_extension_hex
+ local cipher_suites len_ciph_suites len_ciph_suites_byte len_ciph_suites_word
+ local len_client_hello_word len_all_word
+ local ecc_cipher_suite_found=false
+ local extension_signature_algorithms extension_heartbeat session_id
+ local extension_session_ticket extension_next_protocol extension_padding
+ local extension_supported_groups="" extension_supported_point_formats=""
+ local extensions_key_share="" extn_type supported_groups_c2n="" extn_psk_mode=""
+ local extra_extensions extra_extensions_list="" extension_supported_versions=""
+ local offer_compression=false compression_methods
+
+ # TLSv1.3 ClientHello messages MUST specify only the NULL compression method.
+ [[ "$5" == true ]] && [[ "0x$tls_low_byte" -le "0x03" ]] && offer_compression=true
+ [[ "$6" == false ]] && new_socket=false
+
+ cipher_suites="$2" # we don't have the leading \x here so string length is two byte less, see next
+ len_ciph_suites_byte=${#cipher_suites}
+ let "len_ciph_suites_byte += 2"
+
+ # we have additional 2 chars \x in each 2 byte string and 2 byte ciphers, so we need to divide by 4:
+ len_ciph_suites=$(printf "%02x\n" $(( len_ciph_suites_byte / 4 )))
+ len2twobytes "$len_ciph_suites"
+ len_ciph_suites_word="$LEN_STR"
+ #[[ $DEBUG -ge 3 ]] && echo $len_ciph_suites_word
+
+ if [[ "$tls_low_byte" != "00" ]]; then
+ # Add extensions
+
+ # Check to see if any ECC cipher suites are included in cipher_suites
+ # (not needed for TLSv1.3)
+ if [[ "0x$tls_low_byte" -le "0x03" ]]; then
+ for (( i=0; i<len_ciph_suites_byte; i=i+8 )); do
+ j=$i+4
+ part1="0x${cipher_suites:$i:2}"
+ part2="0x${cipher_suites:$j:2}"
+ if [[ "$part1" == 0xc0 ]]; then
+ if [[ "$part2" -ge 0x01 ]] && [[ "$part2" -le 0x19 ]]; then
+ ecc_cipher_suite_found=true && break
+ elif [[ "$part2" -ge 0x23 ]] && [[ "$part2" -le 0x3b ]]; then
+ ecc_cipher_suite_found=true && break
+ elif [[ "$part2" -ge 0x48 ]] && [[ "$part2" -le 0x4f ]]; then
+ ecc_cipher_suite_found=true && break
+ elif [[ "$part2" -ge 0x5c ]] && [[ "$part2" -le 0x63 ]]; then
+ ecc_cipher_suite_found=true && break
+ elif [[ "$part2" -ge 0x70 ]] && [[ "$part2" -le 0x79 ]]; then
+ ecc_cipher_suite_found=true && break
+ elif [[ "$part2" -ge 0x86 ]] && [[ "$part2" -le 0x8d ]]; then
+ ecc_cipher_suite_found=true && break
+ elif [[ "$part2" -ge 0x9a ]] && [[ "$part2" -le 0x9b ]]; then
+ ecc_cipher_suite_found=true && break
+ elif [[ "$part2" -ge 0xac ]] && [[ "$part2" -le 0xaf ]]; then
+ ecc_cipher_suite_found=true && break
+ fi
+ elif [[ "$part1" == 0xcc ]]; then
+ if [[ "$part2" == 0xa8 ]] || [[ "$part2" == 0xa9 ]] || \
+ [[ "$part2" == 0xac ]] || [[ "$part2" == 0x13 ]] || \
+ [[ "$part2" == 0x14 ]]; then
+ ecc_cipher_suite_found=true && break
+ fi
+ fi
+ done
+ fi
+
+ if [[ -n "$SNI" ]]; then
+ #formatted example for SNI
+ #00 00 # extension server_name
+ #00 1a # length = the following +2 = server_name length + 5
+ #00 18 # server_name list_length = server_name length +3
+ #00 # server_name type (hostname)
+ #00 15 # server_name length
+ #66 66 66 66 66 66 2e 66 66 66 66 66 66 66 66 66 66 2e 66 66 66 target.mydomain1.tld # server_name target
+ len_servername=${#NODE}
+ hexdump_format_str="$len_servername/1 \"%02x,\""
+ servername_hexstr=$(printf $NODE | hexdump -v -e "${hexdump_format_str}" | sed 's/,$//')
+ # convert lengths we need to fill in from dec to hex:
+ len_servername_hex=$(printf "%02x\n" $len_servername)
+ len_sni_listlen=$(printf "%02x\n" $((len_servername+3)))
+ len_sni_ext=$(printf "%02x\n" $((len_servername+5)))
+ fi
+
+ if [[ 0x$tls_low_byte -le 0x03 ]]; then
+ extension_signature_algorithms="
+ 00, 0d, # Type: signature_algorithms , see RFC 5246
+ 00, 20, 00,1e, # lengths
+ 06,01, 06,02, 06,03, 05,01, 05,02, 05,03, 04,01, 04,02, 04,03,
+ 03,01, 03,02, 03,03, 02,01, 02,02, 02,03"
+ else
+ extension_signature_algorithms="
+ 00, 0d, # Type: signature_algorithms , see RFC 8446
+ 00, 22, 00, 20, # lengths
+ 04,03, 05,03, 06,03, 08,04, 08,05, 08,06,
+ 04,01, 05,01, 06,01, 08,09, 08,0a, 08,0b,
+ 08,07, 08,08, 02,01, 02,03"
+ fi
+
+ extension_heartbeat="
+ 00, 0f, 00, 01, 01"
+
+ extension_session_ticket="
+ 00, 23, 00, 00"
+
+ extension_next_protocol="
+ 33, 74, 00, 00"
+
+ extn_psk_mode="
+ 00, 2d, 00, 02, 01, 01"
+
+ if "$ecc_cipher_suite_found"; then
+ # Supported Groups Extension
+ extension_supported_groups="
+ 00, 0a, # Type: Supported Elliptic Curves , see RFC 4492
+ 00, 3e, 00, 3c, # lengths
+ 00, 0e, 00, 0d, 00, 19, 00, 1c, 00, 1e, 00, 0b, 00, 0c, 00, 1b,
+ 00, 18, 00, 09, 00, 0a, 00, 1a, 00, 16, 00, 17, 00, 1d, 00, 08,
+ 00, 06, 00, 07, 00, 14, 00, 15, 00, 04, 00, 05, 00, 12, 00, 13,
+ 00, 01, 00, 02, 00, 03, 00, 0f, 00, 10, 00, 11"
+ elif [[ 0x$tls_low_byte -gt 0x03 ]]; then
+ # Supported Groups Extension
+ if [[ ! "$process_full" =~ all ]] || ( [[ ! "$OSSL_NAME" =~ LibreSSL ]] && \
+ ( [[ $OSSL_VER_MAJOR.$OSSL_VER_MINOR == 1.1.1* ]] || \
+ [[ $OSSL_VER_MAJOR.$OSSL_VER_MINOR == 3.0.0* ]] ) ); then
+ extension_supported_groups="
+ 00,0a, # Type: Supported Groups, see RFC 8446
+ 00,10, 00,0e, # lengths
+ 00,1d, 00,17, 00,1e, 00,18, 00,19,
+ 01,00, 01,01"
+ # OpenSSL prior to 1.1.1 does not support X448, so list it as the least
+ # preferred option if the response needs to be decrypted, and do not
+ # list it at all if the response MUST be decrypted.
+ elif [[ $OSSL_VER_MAJOR.$OSSL_VER_MINOR == 1.1.0* ]] && [[ "$process_full" == all+ ]]; then
+ extension_supported_groups="
+ 00,0a, # Type: Supported Groups, see RFC 8446
+ 00,0e, 00,0c, # lengths
+ 00,1d, 00,17, 00,18, 00,19,
+ 01,00, 01,01"
+ elif [[ $OSSL_VER_MAJOR.$OSSL_VER_MINOR == "1.1.0"* ]]; then
+ extension_supported_groups="
+ 00,0a, # Type: Supported Groups, see RFC 8446
+ 00,10, 00,0e, # lengths
+ 00,1d, 00,17, 00,18, 00,19,
+ 01,00, 01,01, 00,1e"
+ # OpenSSL prior to 1.1.0 does not support either X25519 or X448,
+ # so list them as the least referred options if the response
+ # needs to be decrypted, and do not list them at all if the
+ # response MUST be decrypted.
+ elif [[ "$process_full" == all+ ]]; then
+ extension_supported_groups="
+ 00,0a, # Type: Supported Groups, see RFC 8446
+ 00,0c, 00,0a, # lengths
+ 00,17, 00,18, 00,19,
+ 01,00, 01,01"
+ else
+ extension_supported_groups="
+ 00,0a, # Type: Supported Groups, see RFC 8446
+ 00,10, 00,0e, # lengths
+ 00,17, 00,18, 00,19,
+ 01,00, 01,01, 00,1d, 00,1e"
+ fi
+
+ code2network "$extension_supported_groups"
+ supported_groups_c2n="$NW_STR"
+ fi
+
+ if "$ecc_cipher_suite_found" || [[ 0x$tls_low_byte -gt 0x03 ]]; then
+ # Supported Point Formats Extension.
+ extension_supported_point_formats="
+ 00, 0b, # Type: Supported Point Formats , see RFC 4492
+ 00, 02, # len
+ 01, 00"
+ fi
+
+ # Each extension should appear in the ClientHello at most once. So,
+ # find out what extensions were provided as an argument and only use
+ # the provided values for those extensions.
+ extra_extensions="$(tolower "$4")"
+ code2network "$extra_extensions"
+ len_all=${#NW_STR}
+ for (( i=0; i < len_all; i=i+16+4*0x$len_extension_hex )); do
+ part2=$i+4
+ extn_type="${NW_STR:i:2}${NW_STR:part2:2}"
+ extra_extensions_list+=" $extn_type "
+ j=$i+8
+ part2=$j+4
+ len_extension_hex="${NW_STR:j:2}${NW_STR:part2:2}"
+ if [[ "$extn_type" == "000a" ]] && [[ 0x$tls_low_byte -gt 0x03 ]]; then
+ j=14+4*0x$len_extension_hex
+ supported_groups_c2n="${NW_STR:i:j}"
+ fi
+ done
+ if [[ 0x$tls_low_byte -gt 0x03 ]]; then
+ extensions_key_share="$(generate_key_share_extension "$supported_groups_c2n" "$process_full")"
+ [[ $? -ne 0 ]] && return 1
+ fi
+
+ if [[ -n "$SNI" ]] && [[ ! "$extra_extensions_list" =~ " 0000 " ]]; then
+ all_extensions="
+ 00, 00 # extension server_name
+ ,00, $len_sni_ext # length SNI EXT
+ ,00, $len_sni_listlen # server_name list_length
+ ,00 # server_name type (hostname)
+ ,00, $len_servername_hex # server_name length. We assume len(hostname) < FF - 9
+ ,$servername_hexstr" # server_name target
+ fi
+ if [[ 0x$tls_low_byte -ge 0x04 ]] && [[ ! "$extra_extensions_list" =~ " 002b " ]]; then
+ # Add supported_versions extension listing all TLS/SSL versions
+ # from the one specified in $tls_low_byte to SSLv3.
+ for (( i=0x$tls_low_byte; i >=0; i=i-1 )); do
+ if [[ 0x$i -eq 4 ]]; then
+ # FIXME: The ClientHello currently advertises support for various
+ # draft versions of TLSv1.3. Eventually it should only adversize
+ # support for the final version (0304).
+ if [[ "$KEY_SHARE_EXTN_NR" == 33 ]]; then
+ extension_supported_versions+=", 03, 04, 7f, 1c, 7f, 1b, 7f, 1a, 7f, 19, 7f, 18, 7f, 17"
+ else
+ extension_supported_versions+=", 7f, 16, 7f, 15, 7f, 14, 7f, 13, 7f, 12"
+ fi
+ else
+ extension_supported_versions+=", 03, $(printf "%02x" $i)"
+ fi
+ done
+ [[ -n "$all_extensions" ]] && all_extensions+=","
+ # FIXME: Adjust the lengths ("+15" and "+14") when the draft versions of TLSv1.3 are removed.
+ if [[ "$KEY_SHARE_EXTN_NR" == "33" ]]; then
+ all_extensions+="00, 2b, 00, $(printf "%02x" $((2*0x$tls_low_byte+15))), $(printf "%02x" $((2*0x$tls_low_byte+14)))$extension_supported_versions"
+ else
+ all_extensions+="00, 2b, 00, $(printf "%02x" $((2*0x$tls_low_byte+11))), $(printf "%02x" $((2*0x$tls_low_byte+10)))$extension_supported_versions"
+ fi
+ fi
+
+ # There does not seem to be any reason to include this extension. However, it appears that
+ # OpenSSL, Firefox, and Chrome include it in TLS 1.3 ClientHello messages, and there is at
+ # least one server that will fail the connection if it is absent
+ # (see https://github.com/drwetter/testssl.sh/issues/990).
+ if [[ "0x$tls_low_byte" -ge 0x04 ]] && [[ ! "$extra_extensions_list" =~ " 002d " ]]; then
+ [[ -n "$all_extensions" ]] && all_extensions+=","
+ all_extensions+="$extn_psk_mode"
+ fi
+
+ if [[ ! "$extra_extensions_list" =~ " 0023 " ]]; then
+ [[ -n "$all_extensions" ]] && all_extensions+=","
+ all_extensions+="$extension_session_ticket"
+ fi
+
+ # If the ClientHello will include the ALPN extension, then don't include the NPN extension.
+ if [[ ! "$extra_extensions_list" =~ " 3374 " ]] && [[ ! "$extra_extensions_list" =~ " 0010 " ]]; then
+ [[ -n "$all_extensions" ]] && all_extensions+=","
+ all_extensions+="$extension_next_protocol"
+ fi
+
+ # RFC 5246 says that clients MUST NOT offer the signature algorithms
+ # extension if they are offering TLS versions prior to 1.2.
+ if [[ "0x$tls_low_byte" -ge 0x03 ]] && [[ ! "$extra_extensions_list" =~ " 000d " ]]; then
+ [[ -n "$all_extensions" ]] && all_extensions+=","
+ all_extensions+="$extension_signature_algorithms"
+ fi
+
+ if [[ -n "$extension_supported_groups" ]] && [[ ! "$extra_extensions_list" =~ " 000a " ]]; then
+ [[ -n "$all_extensions" ]] && all_extensions+=","
+ all_extensions+="$extension_supported_groups"
+ fi
+
+ if [[ -n "$extensions_key_share" ]] && [[ ! "$extra_extensions_list" =~ " 00$KEY_SHARE_EXTN_NR " ]]; then
+ [[ -n "$all_extensions" ]] && all_extensions+=","
+ all_extensions+="$extensions_key_share"
+ fi
+
+ if [[ -n "$extension_supported_point_formats" ]] && [[ ! "$extra_extensions_list" =~ " 000b " ]]; then
+ [[ -n "$all_extensions" ]] && all_extensions+=","
+ all_extensions+="$extension_supported_point_formats"
+ fi
+
+ if [[ -n "$extra_extensions" ]]; then
+ [[ -n "$all_extensions" ]] && all_extensions+=","
+ all_extensions+="$extra_extensions"
+ fi
+
+ # Make sure that a non-empty extension goes last (either heartbeat or padding).
+ # See PR #792 and https://www.ietf.org/mail-archive/web/tls/current/msg19720.html.
+ if [[ ! "$extra_extensions_list" =~ " 000f " ]]; then
+ [[ -n "$all_extensions" ]] && all_extensions+=","
+ all_extensions+="$extension_heartbeat"
+ fi
+
+ code2network "$all_extensions" # convert extensions
+ all_extensions="$NW_STR" # we don't have the leading \x here so string length is two byte less, see next
+ len_extension=${#all_extensions}
+ len_extension+=2
+ len_extension=$len_extension/4
+ len_extension_hex=$(printf "%02x\n" $len_extension)
+
+ # If the length of the Client Hello would be between 256 and 511 bytes,
+ # then add a padding extension (see RFC 7685)
+ len_all=$((0x$len_ciph_suites + 0x2b + 0x$len_extension_hex + 0x2))
+ "$offer_compression" && len_all+=2
+ [[ 0x$tls_low_byte -gt 0x03 ]] && len_all+=32 # TLSv1.3 ClientHello includes a 32-byte session id
+ if [[ $len_all -ge 256 ]] && [[ $len_all -le 511 ]] && [[ ! "$extra_extensions_list" =~ " 0015 " ]]; then
+ if [[ $len_all -ge 508 ]]; then
+ len_padding_extension=1 # Final extension cannot be empty: see PR #792
+ else
+ len_padding_extension=$((508 - len_all))
+ fi
+ len_padding_extension_hex=$(printf "%02x\n" $len_padding_extension)
+ len2twobytes "$len_padding_extension_hex"
+ all_extensions="$all_extensions\\x00\\x15\\x${LEN_STR:0:2}\\x${LEN_STR:4:2}"
+ for (( i=0; i<len_padding_extension; i++ )); do
+ all_extensions="$all_extensions\\x00"
+ done
+ len_extension=$len_extension+$len_padding_extension+0x4
+ len_extension_hex=$(printf "%02x\n" $len_extension)
+ elif [[ ! "$extra_extensions_list" =~ " 0015 " ]] && ( [[ $((len_all%256)) -eq 10 ]] || [[ $((len_all%256)) -eq 14 ]] ); then
+ # Some servers fail if the length of the ClientHello is 522, 778, 1034, 1290, ... bytes.
+ # A few servers also fail if the length is 526, 782, 1038, 1294, ... bytes.
+ # So, if the ClientHello would be one of these length, add a 5-byte padding extension.
+ all_extensions="$all_extensions\\x00\\x15\\x00\\x01\\x00"
+ len_extension+=5
+ len_extension_hex=$(printf "%02x\n" $len_extension)
+ fi
+ len2twobytes "$len_extension_hex"
+ all_extensions="
+ ,$LEN_STR # first the len of all extensions.
+ ,$all_extensions"
+ fi
+
+ if [[ 0x$tls_low_byte -gt 0x03 ]]; then
+ # TLSv1.3 calls for sending a random 32-byte session id in middlebox compatibility mode.
+ session_id="20,44,b8,92,56,af,74,52,9e,d8,cf,52,14,c8,af,d8,34,0b,e7,7f,eb,86,01,84,50,5d,e4,a1,6a,09,3b,bf,6e"
+ len_session_id=32
+ else
+ session_id="00"
+ len_session_id=0
+ fi
+
+ # RFC 3546 doesn't specify SSLv3 to have SNI, openssl just ignores the switch if supplied
+ if [[ "$tls_low_byte" == "00" ]]; then
+ len_all=$((0x$len_ciph_suites + len_session_id + 0x27))
+ else
+ len_all=$((0x$len_ciph_suites + len_session_id + 0x27 + 0x$len_extension_hex + 0x2))
+ fi
+ "$offer_compression" && len_all+=2
+ len2twobytes $(printf "%02x\n" $len_all)
+ len_client_hello_word="$LEN_STR"
+ #[[ $DEBUG -ge 3 ]] && echo $len_client_hello_word
+
+ if [[ "$tls_low_byte" == 00 ]]; then
+ len_all=$((0x$len_ciph_suites + len_session_id + 0x2b))
+ else
+ len_all=$((0x$len_ciph_suites + len_session_id + 0x2b + 0x$len_extension_hex + 0x2))
+ fi
+ "$offer_compression" && len_all+=2
+ len2twobytes $(printf "%02x\n" $len_all)
+ len_all_word="$LEN_STR"
+ #[[ $DEBUG -ge 3 ]] && echo $len_all_word
+
+ # if we have SSLv3, the first occurrence of TLS protocol -- record layer -- is SSLv3, otherwise TLS 1.0,
+ # except in the case of a second ClientHello in TLS 1.3, in which case it is TLS 1.2.
+ [[ $tls_low_byte == "00" ]] && tls_word_reclayer="03, 00"
+
+ [[ 0x$tls_legacy_version -ge 0x04 ]] && tls_legacy_version="03"
+
+ if "$offer_compression"; then
+ # See https://www.iana.org/assignments/comp-meth-ids/comp-meth-ids.xhtml#comp-meth-ids-2
+ compression_methods="03,01,40,00" # Offer NULL, DEFLATE, and LZS compression
+ else
+ compression_methods="01,00" # Only offer NULL compression (0x00)
+ fi
+
+ TLS_CLIENT_HELLO="
+ # TLS header ( 5 bytes)
+ ,16, $tls_word_reclayer # TLS Version: in wireshark this is always 01 for TLS 1.0-1.2
+ ,$len_all_word # Length <---
+ # Handshake header:
+ ,01 # Type (x01 for ClientHello)
+ ,00, $len_client_hello_word # Length ClientHello
+ ,03, $tls_legacy_version # TLS version ClientHello
+ ,54, 51, 1e, 7a # Unix time since see www.moserware.com/2009/06/first-few-milliseconds-of-https.html
+ ,de, ad, be, ef # Random 28 bytes
+ ,31, 33, 07, 00, 00, 00, 00, 00
+ ,cf, bd, 39, 04, cc, 16, 0b, 85
+ ,03, 90, 9f, 77, 04, 33, d4, de
+ ,$session_id
+ ,$len_ciph_suites_word # Cipher suites length
+ ,$cipher_suites
+ ,$compression_methods"
+
+ if "$new_socket"; then
+ fd_socket 5 || return 6
+ fi
+
+ debugme echo -n "sending client hello... "
+ socksend_clienthello "$TLS_CLIENT_HELLO$all_extensions" $USLEEP_SND
+
+ if [[ "$tls_low_byte" -gt 0x03 ]]; then
+ TLS_CLIENT_HELLO="$(tolower "$NW_STR")"
+ TLS_CLIENT_HELLO="${TLS_CLIENT_HELLO//\\x0\\/\\x00\\}"
+ TLS_CLIENT_HELLO="${TLS_CLIENT_HELLO//\\x1\\/\\x01\\}"
+ TLS_CLIENT_HELLO="${TLS_CLIENT_HELLO//\\x2\\/\\x02\\}"
+ TLS_CLIENT_HELLO="${TLS_CLIENT_HELLO//\\x3\\/\\x03\\}"
+ TLS_CLIENT_HELLO="${TLS_CLIENT_HELLO//\\x4\\/\\x04\\}"
+ TLS_CLIENT_HELLO="${TLS_CLIENT_HELLO//\\x5\\/\\x05\\}"
+ TLS_CLIENT_HELLO="${TLS_CLIENT_HELLO//\\x6\\/\\x06\\}"
+ TLS_CLIENT_HELLO="${TLS_CLIENT_HELLO//\\x7\\/\\x07\\}"
+ TLS_CLIENT_HELLO="${TLS_CLIENT_HELLO//\\x8\\/\\x08\\}"
+ TLS_CLIENT_HELLO="${TLS_CLIENT_HELLO//\\x9\\/\\x09\\}"
+ TLS_CLIENT_HELLO="${TLS_CLIENT_HELLO//\\xa\\/\\x0a\\}"
+ TLS_CLIENT_HELLO="${TLS_CLIENT_HELLO//\\xb\\/\\x0b\\}"
+ TLS_CLIENT_HELLO="${TLS_CLIENT_HELLO//\\xc\\/\\x0c\\}"
+ TLS_CLIENT_HELLO="${TLS_CLIENT_HELLO//\\xd\\/\\x0d\\}"
+ TLS_CLIENT_HELLO="${TLS_CLIENT_HELLO//\\xe\\/\\x0e\\}"
+ TLS_CLIENT_HELLO="${TLS_CLIENT_HELLO//\\xf\\/\\x0f\\}"
+ TLS_CLIENT_HELLO="${TLS_CLIENT_HELLO//\\x/}"
+ TLS_CLIENT_HELLO="${TLS_CLIENT_HELLO:10}"
+ fi
+
+ return 0
+}
+
+# arg1: The original ClientHello
+# arg2: The server's response
+# Return 0 if the response is not a HelloRetryRequest.
+# Return 1 if the response is a malformed HelloRetryRequest or if a new ClientHello cannot be sent.
+# Return 2 if the response is a HelloRetryRequest, and sending a new ClientHello succeeded.
+# Return 6 if the response is a HelloRetryRequest, and sending a new ClientHello failed.
+resend_if_hello_retry_request() {
+ local original_clienthello="$1"
+ local tls_hello_ascii="$2"
+ local msg_type tls_low_byte server_version cipher_suite rfc_cipher_suite
+ local key_share="" new_key_share="" cookie="" second_clienthello data=""
+ local -i i j msg_len tls_hello_ascii_len sid_len
+ local -i extns_offset hrr_extns_len extra_extensions_len len_extn
+ local extra_extensions extn_type part2 new_extra_extns=""
+ local sha256_hrr="CF21AD74E59A6111BE1D8C021E65B891C2A211167ABB8C5E079E09E2C8A8339C"
+
+ tls_hello_ascii_len=${#tls_hello_ascii}
+ # A HelloRetryRequest is at least 13 bytes long
+ [[ $tls_hello_ascii_len -lt 26 ]] && return 0
+ # A HelloRetryRequest is a handshake message (16) with a major record version of 03.
+ [[ "${tls_hello_ascii:0:4}" != 1603 ]] && return 0
+ msg_type="${tls_hello_ascii:10:2}"
+ if [[ "$msg_type" == 02 ]]; then
+ # A HRR is a ServerHello with a Random value equal to the
+ # SHA-256 hash of "HelloRetryRequest"
+ [[ $tls_hello_ascii_len -lt 76 ]] && return 0
+ [[ "${tls_hello_ascii:22:64}" != $sha256_hrr ]] && return 0
+ elif [[ "$msg_type" != 06 ]]; then
+ # The handshake type for hello_retry_request in draft versions was 06.
+ return 0
+ fi
+
+ # This appears to be a HelloRetryRequest message.
+ debugme echo "reading hello retry request... "
+ if [[ "$DEBUG" -ge 4 ]]; then
+ hexdump -C $SOCK_REPLY_FILE | head -6
+ echo
+ [[ "$DEBUG" -ge 5 ]] && echo "$tls_hello_ascii" # one line without any blanks
+ fi
+
+ # Check the length of the handshake message
+ msg_len=2*$(hex2dec "${tls_hello_ascii:6:4}")
+ if [[ $msg_len -gt $tls_hello_ascii_len-10 ]]; then
+ debugme echo "malformed HelloRetryRequest"
+ return 1
+ fi
+ # The HelloRetryRequest message may be followed by something
+ # else (e.g., a change cipher spec message). Ignore anything
+ # that follows.
+ tls_hello_ascii_len=$msg_len+10
+
+ # Check the length of the HelloRetryRequest message.
+ msg_len=2*$(hex2dec "${tls_hello_ascii:12:6}")
+ if [[ $msg_len -ne $tls_hello_ascii_len-18 ]]; then
+ debugme echo "malformed HelloRetryRequest"
+ return 1
+ fi
+
+ if [[ "$msg_type" == 06 ]]; then
+ server_version="${tls_hello_ascii:18:4}"
+ if [[ 0x$server_version -ge 0x7f13 ]]; then
+ # Starting with TLSv1.3 draft 19, a HelloRetryRequest is at least 15 bytes long
+ [[ $tls_hello_ascii_len -lt 30 ]] && return 0
+ cipher_suite="${tls_hello_ascii:22:2},${tls_hello_ascii:24:2}"
+ extns_offset=26
+ else
+ extns_offset=22
+ fi
+ else
+ sid_len=2*$(hex2dec "${tls_hello_ascii:86:2}")
+ i=88+$sid_len
+ j=90+$sid_len
+ cipher_suite="${tls_hello_ascii:i:2},${tls_hello_ascii:j:2}"
+ extns_offset=94+$sid_len
+ fi
+
+ # Check the length of the extensions.
+ hrr_extns_len=2*$(hex2dec "${tls_hello_ascii:extns_offset:4}")
+ if [[ $hrr_extns_len -ne $tls_hello_ascii_len-$extns_offset-4 ]]; then
+ debugme echo "malformed HelloRetryRequest"
+ return 1
+ fi
+
+ # Parse HelloRetryRequest extensions
+ for (( i=extns_offset+4; i < tls_hello_ascii_len; i=i+8+len_extn )); do
+ extn_type="${tls_hello_ascii:i:4}"
+ j=$i+4
+ len_extn=2*$(hex2dec "${tls_hello_ascii:j:4}")
+ j+=4
+ if [[ $len_extn -gt $tls_hello_ascii_len-$j ]]; then
+ debugme echo "malformed HelloRetryRequest"
+ return 1
+ fi
+ if [[ "$extn_type" == 002C ]]; then
+ # If the HRR includes a cookie extension, then it needs to be
+ # included in the next ClientHello.
+ j=8+$len_extn
+ cookie="${tls_hello_ascii:i:j}"
+ elif [[ "$extn_type" == 00$KEY_SHARE_EXTN_NR ]]; then
+ # If the HRR includes a key_share extension, then it specifies the
+ # group to be used in the next ClientHello. So, create a key_share
+ # extension that specifies this group.
+ if [[ $len_extn -ne 4 ]]; then
+ debugme echo "malformed key share extension in HelloRetryRequest"
+ return 1
+ fi
+ key_share="${tls_hello_ascii:j:4}"
+ new_key_share="$(generate_key_share_extension "000a00040002$key_share" "ephemeralkey")"
+ [[ $? -ne 0 ]] && return 1
+ [[ -z "$new_key_share" ]] && return 1
+ new_key_share="${new_key_share//,/}"
+ elif [[ "$extn_type" == 002B ]]; then
+ if [[ $len_extn -ne 4 ]]; then
+ debugme echo "malformed supported versions extension in HelloRetryRequest"
+ return 1
+ fi
+ server_version="${tls_hello_ascii:j:4}"
+ fi
+ done
+
+ if [[ $DEBUG -ge 3 ]]; then
+ echo "TLS message fragments:"
+ echo " tls_protocol (reclyr): 0x${tls_hello_ascii:2:4}"
+ echo " tls_content_type: 0x16 (handshake)"
+ echo " msg_len: $(hex2dec "${tls_hello_ascii:6:4}")"
+ echo
+ echo "TLS handshake message:"
+ echo -n " handshake type: 0x$msg_type "
+ case "$msg_type" in
+ 02) echo "(hello_retry_request formatted as server_hello)" ;;
+ 06) echo "(hello_retry_request)" ;;
+ esac
+ echo " msg_len: $(hex2dec "${tls_hello_ascii:12:6}")"
+ echo
+ echo "TLS hello retry request message:"
+ echo " server version: $server_version"
+ if [[ "$server_version" == 0304 ]] || [[ 0x$server_version -ge 0x7f13 ]]; then
+ echo -n " cipher suite: $cipher_suite"
+ if [[ $TLS_NR_CIPHERS -ne 0 ]]; then
+ if [[ "${cipher_suite:0:2}" == "00" ]]; then
+ rfc_cipher_suite="$(show_rfc_style "x${cipher_suite:3:2}")"
+ else
+ rfc_cipher_suite="$(show_rfc_style "x${cipher_suite:0:2}${cipher_suite:3:2}")"
+ fi
+ elif "$HAS_CIPHERSUITES"; then
+ rfc_cipher_suite="$($OPENSSL ciphers -V -ciphersuites "$TLS13_OSSL_CIPHERS" 'ALL:COMPLEMENTOFALL' | grep -i " 0x${cipher_suite:0:2},0x${cipher_suite:3:2} " | awk '{ print $3 }')"
+ else
+ rfc_cipher_suite="$($OPENSSL ciphers -V 'ALL:COMPLEMENTOFALL' | grep -i " 0x${cipher_suite:0:2},0x${cipher_suite:3:2} " | awk '{ print $3 }')"
+ fi
+ if [[ -n "$rfc_cipher_suite" ]]; then
+ echo " ($rfc_cipher_suite)"
+ else
+ echo ""
+ fi
+ fi
+ [[ -n "$key_share" ]] && echo " key share: 0x$key_share"
+ [[ -n "$cookie" ]] && echo " cookie: $cookie"
+ fi
+
+ # Starting with TLSv1.3 draft 24, the second ClientHello should specify a record layer version of 0x0303
+ if [[ "$server_version" == 0304 ]] || [[ 0x$server_version -ge 0x7f18 ]]; then
+ original_clienthello="160303${original_clienthello:6}"
+ fi
+
+ if [[ "$server_version" == 0304 ]] || [[ 0x$server_version -ge 0x7f16 ]]; then
+ # Send a dummy change cipher spec for middlebox compatibility.
+ debugme echo -en "\nsending dummy change cipher spec... "
+ socksend ", x14, x03, x03 ,x00, x01, x01" 0
+ fi
+ debugme echo -en "\nsending second client hello... "
+ second_clienthello="$(modify_clienthello "$original_clienthello" "$new_key_share" "$cookie")"
+ TLS_CLIENT_HELLO="${second_clienthello:10}"
+ msg_len=${#second_clienthello}
+ for (( i=0; i < msg_len; i=i+2 )); do
+ data+=", ${second_clienthello:i:2}"
+ done
+ debugme echo -n "sending client hello... "
+ socksend_clienthello "$data" $USLEEP_SND
+ sockread_serverhello 32768
+ return 2
+}
+
+# arg1: TLS version low byte
+# (00: SSLv3, 01: TLS 1.0, 02: TLS 1.1, 03: TLS 1.2)
+# arg2: (optional) list of cipher suites
+# arg3: (optional): "all" - process full response (including Certificate and certificate_status handshake messages)
+# "all+" - same as "all", but do not offer any curves with TLSv1.3 that are not supported by
+# $OPENSSL, since response MUST be decrypted.
+# "ephemeralkey" - extract the server's ephemeral key (if any)
+# arg4: (optional) additional request extensions
+# arg5: (optional) "true" if ClientHello should advertise compression methods other than "NULL"
+# arg6: (optional) "false" if the connection should not be closed before the function returns.
+# return: 0: successful connect | 1: protocol or cipher not available | 2: as (0) but downgraded
+# 6: couldn't open socket | 7: couldn't open temp file
+tls_sockets() {
+ local -i ret=0
+ local -i save=0
+ local lines
+ local tls_low_byte
+ local cipher_list_2send
+ local sock_reply_file2 sock_reply_file3
+ local tls_hello_ascii next_packet
+ local clienthello1 original_clienthello hrr=""
+ local process_full="$3" offer_compression=false skip=false
+ local close_connection=true
+ local -i hello_done=0
+ local cipher="" key_and_iv="" decrypted_response
+
+ [[ "$5" == true ]] && offer_compression=true
+ [[ "$6" == false ]] && close_connection=false
+ tls_low_byte="$1"
+ if [[ -n "$2" ]]; then # use supplied string in arg2 if there is one
+ cipher_list_2send="$2"
+ else # otherwise use std ciphers then
+ if [[ "$tls_low_byte" == 03 ]]; then
+ cipher_list_2send="$TLS12_CIPHER"
+ else
+ cipher_list_2send="$TLS_CIPHER"
+ fi
+ fi
+ code2network "$(tolower "$cipher_list_2send")" # convert CIPHER_SUITES to a "standardized" format
+ cipher_list_2send="$NW_STR"
+
+ debugme echo -en "\nsending client hello... "
+ prepare_tls_clienthello "$tls_low_byte" "$cipher_list_2send" "$process_full" "$4" "$offer_compression"
+ ret=$? # 6 means opening socket didn't succeed, e.g. timeout
+
+ # if sending didn't succeed we don't bother
+ if [[ $ret -eq 0 ]]; then
+ clienthello1="$TLS_CLIENT_HELLO"
+ sockread_serverhello 32768
+ "$TLS_DIFFTIME_SET" && TLS_NOW=$(LC_ALL=C date "+%s")
+
+ tls_hello_ascii=$(hexdump -v -e '16/1 "%02X"' "$SOCK_REPLY_FILE")
+ tls_hello_ascii="${tls_hello_ascii%%[!0-9A-F]*}"
+ tls_hello_ascii="${tls_hello_ascii%%140303000101}"
+
+ # Check if the response is a HelloRetryRequest.
+ original_clienthello="160301$(printf "%04x" "${#clienthello1}")$clienthello1"
+ resend_if_hello_retry_request "$original_clienthello" "$tls_hello_ascii"
+ ret=$?
+ if [[ $ret -eq 2 ]]; then
+ hrr="${tls_hello_ascii:10}"
+ tls_hello_ascii=$(hexdump -v -e '16/1 "%02X"' "$SOCK_REPLY_FILE")
+ tls_hello_ascii="${tls_hello_ascii%%[!0-9A-F]*}"
+ elif [[ $ret -eq 1 ]] || [[ $ret -eq 6 ]]; then
+ close_socket
+ TMPFILE=$SOCK_REPLY_FILE
+ tmpfile_handle ${FUNCNAME[0]}.dd
+ return $ret
+ fi
+
+ # The server's response may span more than one packet. If only the
+ # first part of the response needs to be processed, this isn't an
+ # issue. However, if the entire response needs to be processed or
+ # if the ephemeral key is needed (which comes last for TLS 1.2 and
+ # below), then we need to check if response appears to be complete,
+ # and if it isn't then try to get another packet from the server.
+ if [[ "$process_full" =~ all ]] || [[ "$process_full" == ephemeralkey ]]; then
+ hello_done=1; skip=true
+ fi
+ for (( 1 ; hello_done==1; 1 )); do
+ if ! "$skip"; then
+ if [[ $DEBUG -ge 1 ]]; then
+ sock_reply_file2=$(mktemp $TEMPDIR/ddreply.XXXXXX) || return 7
+ mv "$SOCK_REPLY_FILE" "$sock_reply_file2"
+ fi
+
+ debugme echo -n "requesting more server hello data... "
+ socksend "" $USLEEP_SND
+ sockread_serverhello 32768
+
+ next_packet=$(hexdump -v -e '16/1 "%02X"' "$SOCK_REPLY_FILE")
+ next_packet="${next_packet%%[!0-9A-F]*}"
+
+ if [[ ${#next_packet} -eq 0 ]]; then
+ # This shouldn't be necessary. However, it protects against
+ # getting into an infinite loop if the server has nothing
+ # left to send and check_tls_serverhellodone doesn't
+ # correctly catch it.
+ [[ $DEBUG -ge 1 ]] && mv "$sock_reply_file2" "$SOCK_REPLY_FILE"
+ hello_done=0
+ else
+ tls_hello_ascii+="$next_packet"
+ if [[ $DEBUG -ge 1 ]]; then
+ sock_reply_file3=$(mktemp $TEMPDIR/ddreply.XXXXXX) || return 7
+ mv "$SOCK_REPLY_FILE" "$sock_reply_file3"
+ mv "$sock_reply_file2" "$SOCK_REPLY_FILE"
+ cat "$sock_reply_file3" >> "$SOCK_REPLY_FILE"
+ rm "$sock_reply_file3"
+ fi
+ fi
+ fi
+ skip=false
+ if [[ $hello_done -eq 1 ]]; then
+ decrypted_response="$(check_tls_serverhellodone "$tls_hello_ascii" "$process_full" "$cipher" "$key_and_iv")"
+ hello_done=$?
+ [[ "$hello_done" -eq 0 ]] && [[ -n "$decrypted_response" ]] && tls_hello_ascii="$(toupper "$decrypted_response")"
+ if [[ "$hello_done" -eq 3 ]]; then
+ hello_done=1; skip=true
+ debugme echo "reading server hello..."
+ parse_tls_serverhello "$tls_hello_ascii" "ephemeralkey"
+ ret=$?
+ if [[ "$ret" -eq 0 ]] || [[ "$ret" -eq 2 ]]; then
+ cipher=$(get_cipher "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt")
+ if [[ -n "$hrr" ]]; then
+ key_and_iv="$(derive-handshake-traffic-keys "$cipher" "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt" "$clienthello1" "$hrr" "$TLS_CLIENT_HELLO" "$TLS_SERVER_HELLO")"
+ else
+ key_and_iv="$(derive-handshake-traffic-keys "$cipher" "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt" "" "" "$TLS_CLIENT_HELLO" "$TLS_SERVER_HELLO")"
+ fi
+ [[ $? -ne 0 ]] && hello_done=2
+ else
+ hello_done=2
+ fi
+ fi
+ fi
+ done
+
+ debugme echo "reading server hello..."
+ if [[ "$DEBUG" -ge 4 ]]; then
+ hexdump -C $SOCK_REPLY_FILE | head -6
+ echo
+ fi
+
+ parse_tls_serverhello "$tls_hello_ascii" "$process_full" "$cipher_list_2send"
+ save=$?
+ if "$close_connection" && [[ $save == 0 ]]; then
+ send_close_notify "$DETECTED_TLS_VERSION"
+ fi
+
+ if [[ $DEBUG -ge 2 ]]; then
+ # see https://secure.wand.net.nz/trac/libprotoident/wiki/SSL
+ lines=$(count_lines "$(hexdump -C "$SOCK_REPLY_FILE" 2>$ERRFILE)")
+ tm_out " ($lines lines returned) "
+ fi
+
+ # determine the return value for higher level, so that they can tell what the result is
+ if [[ $save -eq 1 ]] || [[ $lines -eq 1 ]]; then
+ ret=1 # NOT available
+ elif [[ $save -eq 3 ]]; then
+ # only for IMAP currently 'a002 NO Starttls'
+ ret=3
+ elif [[ $save -eq 8 ]]; then
+ # odd return, we just pass this from parse_tls_serverhello() back
+ ret=8
+ elif [[ $save -eq 4 ]]; then
+ # STARTTLS problem passing back
+ ret=4
+ else
+ if [[ 03$tls_low_byte -eq $DETECTED_TLS_VERSION ]]; then
+ ret=0 # protocol available, TLS version returned equal to the one send
+ else
+ debugme echo -n "protocol send: 0x03$tls_low_byte, returned: 0x$DETECTED_TLS_VERSION"
+ ret=2 # protocol NOT available, server downgraded to $DETECTED_TLS_VERSION
+ fi
+ fi
+ debugme echo
+ else
+ debugme echo "stuck on sending: $ret"
+ fi
+
+ "$close_connection" && close_socket
+ tmpfile_handle ${FUNCNAME[0]}.dd $SOCK_REPLY_FILE
+ return $ret
+}
+
+
+####### Vulnerabilities follow #######
+# General overview which browser "supports" which vulnerability:
+# https://en.wikipedia.org/wiki/Transport_Layer_Security-SSL#Web_browsers
+
+# mainly adapted from https://gist.github.com/takeshixx/10107280
+#
+run_heartbleed(){
+ local tls_hexcode
+ local heartbleed_payload
+ local -i n lines_returned
+ local append=""
+ local tls_hello_ascii=""
+ local jsonID="heartbleed"
+ local cve="CVE-2014-0160"
+ local cwe="CWE-119"
+ local hint=""
+
+ [[ $VULN_COUNT -le $VULN_THRESHLD ]] && outln && pr_headlineln " Testing for heartbleed vulnerability " && outln
+ pr_bold " Heartbleed"; out " ($cve) "
+
+ if ( [[ "$STARTTLS_PROTOCOL" =~ ldap ]] || [[ "$STARTTLS_PROTOCOL" =~ irc ]] ); then
+ prln_local_problem "STARTTLS/$STARTTLS_PROTOCOL and --ssl-native collide here"
+ return 1
+ fi
+
+ [[ -z "$TLS_EXTENSIONS" ]] && determine_tls_extensions
+ if [[ ! "${TLS_EXTENSIONS}" =~ heartbeat ]]; then
+ pr_svrty_best "not vulnerable (OK)"
+ outln ", no heartbeat extension"
+ fileout "$jsonID" "OK" "not vulnerable, no heartbeat extension" "$cve" "$cwe"
+ return 0
+ fi
+
+ if [[ 0 -eq $(has_server_protocol tls1) ]]; then
+ tls_hexcode="x03, x01"
+ elif [[ 0 -eq $(has_server_protocol tls1_1) ]]; then
+ tls_hexcode="x03, x02"
+ elif [[ 0 -eq $(has_server_protocol tls1_2) ]]; then
+ tls_hexcode="x03, x03"
+ elif [[ 0 -eq $(has_server_protocol ssl3) ]]; then
+ tls_hexcode="x03, x00"
+ else # no protocol for some reason defined, determine TLS versions offered with a new handshake
+ $OPENSSL s_client $(s_client_options "$STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY") >$TMPFILE 2>$ERRFILE </dev/null
+ case "$(get_protocol $TMPFILE)" in
+ *1.2) tls_hexcode="x03, x03" ; add_tls_offered tls1_2 yes ;;
+ *1.1) tls_hexcode="x03, x02" ; add_tls_offered tls1_1 yes ;;
+ TLSv1) tls_hexcode="x03, x01" ; add_tls_offered tls1 yes ;;
+ SSLv3) tls_hexcode="x03, x00" ; add_tls_offered ssl3 yes ;;
+ esac
+ fi
+ debugme echo "using protocol $tls_hexcode"
+
+ heartbleed_payload=", x18, $tls_hexcode, x00, x03, x01, x40, x00"
+ tls_sockets "${tls_hexcode:6:2}" "" "ephemeralkey" "" "" "false"
+
+ [[ $DEBUG -ge 4 ]] && tmln_out "\nsending payload with TLS version $tls_hexcode:"
+ socksend "$heartbleed_payload" 1
+ sockread_serverhello 16384 $HEARTBLEED_MAX_WAITSOCK
+ if [[ $? -eq 3 ]]; then
+ append=", timed out"
+ pr_svrty_best "not vulnerable (OK)"; out "$append"
+ fileout "$jsonID" "OK" "not vulnerable $append" "$cve" "$cwe"
+ else
+
+ # server reply should be (>=SSLv3): 18030x in case of a heartBEAT reply -- which we take as a positive result
+ tls_hello_ascii=$(hexdump -v -e '16/1 "%02X"' "$SOCK_REPLY_FILE")
+ debugme echo "tls_content_type: ${tls_hello_ascii:0:2}"
+ debugme echo "tls_protocol: ${tls_hello_ascii:2:4}"
+
+ lines_returned=$(count_lines "$(hexdump -ve '16/1 "%02x " " \n"' "$SOCK_REPLY_FILE")")
+ debugme echo "lines HB reply: $lines_returned"
+
+ if [[ $DEBUG -ge 3 ]]; then
+ tmln_out "\nheartbleed reply: "
+ hexdump -C "$SOCK_REPLY_FILE" | head -20
+ [[ $lines_returned -gt 20 ]] && tmln_out "[...]"
+ tmln_out
+ fi
+
+ if [[ $lines_returned -gt 1 ]] && [[ "${tls_hello_ascii:0:4}" == 1803 ]]; then
+ if [[ "$STARTTLS_PROTOCOL" =~ ftp ]]; then
+ # check possibility of weird vsftpd reply, see #426, despite "1803" seems very unlikely...
+ if grep -q '500 OOPS' "$SOCK_REPLY_FILE" ; then
+ append=", successful weeded out vsftpd false positive"
+ pr_svrty_best "not vulnerable (OK)"; out "$append"
+ fileout "$jsonID" "OK" "not vulnerable $append" "$cve" "$cwe"
+ else
+ out "likely "
+ pr_svrty_critical "VULNERABLE (NOT ok)"
+ [[ $DEBUG -lt 3 ]] && tm_out ", use debug >=3 to confirm"
+ fileout "$jsonID" "CRITICAL" "VULNERABLE" "$cve" "$cwe" "$hint"
+ fi
+ else
+ pr_svrty_critical "VULNERABLE (NOT ok)"
+ fileout "$jsonID" "CRITICAL" "VULNERABLE" "$cve" "$cwe" "$hint"
+ fi
+ else
+ pr_svrty_best "not vulnerable (OK)"
+ fileout "$jsonID" "OK" "not vulnerable" "$cve" "$cwe"
+ fi
+ fi
+ outln
+ tmpfile_handle ${FUNCNAME[0]}.dd $SOCK_REPLY_FILE
+ close_socket
+ return 0
+}
+
+# helper function
+ok_ids(){
+ prln_svrty_best "\n ok -- something reset our ccs packets"
+ return 0
+}
+
+# see https://www.openssl.org/news/secadv_20140605.txt
+# mainly adapted from Ramon de C Valle's C code from https://gist.github.com/rcvalle/71f4b027d61a78c42607
+#FIXME: At a certain point ccs needs to be changed and make use of code2network using a file, then tls_sockets
+#
+run_ccs_injection(){
+ local tls_hexcode ccs_message client_hello byte6
+ local -i retval ret=0
+ local tls_hello_ascii=""
+ local jsonID="CCS"
+ local cve="CVE-2014-0224"
+ local cwe="CWE-310"
+ local hint=""
+
+ [[ $VULN_COUNT -le $VULN_THRESHLD ]] && outln && pr_headlineln " Testing for CCS injection vulnerability " && outln
+ pr_bold " CCS"; out " ($cve) "
+
+ if ( [[ "$STARTTLS_PROTOCOL" =~ ldap ]] || [[ "$STARTTLS_PROTOCOL" =~ irc ]] ); then
+ prln_local_problem "STARTTLS/$STARTTLS_PROTOCOL and --ssl-native collide here"
+ return 1
+ fi
+
+ if [[ 0 -eq $(has_server_protocol tls1) ]]; then
+ tls_hexcode="x03, x01"
+ elif [[ 0 -eq $(has_server_protocol tls1_1) ]]; then
+ tls_hexcode="x03, x02"
+ elif [[ 0 -eq $(has_server_protocol tls1_2) ]]; then
+ tls_hexcode="x03, x03"
+ elif [[ 0 -eq $(has_server_protocol ssl3) ]]; then
+ tls_hexcode="x03, x00"
+ else # no protocol for some reason defined, determine TLS versions offered with a new handshake
+ $OPENSSL s_client $(s_client_options "$STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY") >$TMPFILE 2>$ERRFILE </dev/null
+ case "$(get_protocol $TMPFILE)" in
+ *1.2) tls_hexcode="x03, x03" ; add_tls_offered tls1_2 yes ;;
+ *1.1) tls_hexcode="x03, x02" ; add_tls_offered tls1_1 yes ;;
+ TLSv1) tls_hexcode="x03, x01" ; add_tls_offered tls1 yes ;;
+ SSLv3) tls_hexcode="x03, x00" ; add_tls_offered ssl3 yes ;;
+ esac
+ fi
+ debugme echo "using protocol $tls_hexcode"
+
+ ccs_message=", x14, $tls_hexcode ,x00, x01, x01"
+
+ client_hello="
+ # TLS header (5 bytes)
+ ,x16, # content type (x16 for handshake)
+ x03, x01, # TLS version in record layer is always TLS 1.0 (except SSLv3)
+ x00, x93, # length
+ # Handshake header
+ x01, # type (x01 for ClientHello)
+ x00, x00, x8f, # length
+ $tls_hexcode, # TLS version
+ # Random (32 byte)
+ x53, x43, x5b, x90, x9d, x9b, x72, x0b,
+ xbc, x0c, xbc, x2b, x92, xa8, x48, x97,
+ xcf, xbd, x39, x04, xcc, x16, x0b, x85,
+ x03, x90, x9f, x77, x04, x33, xd4, xde,
+ x00, # session ID length
+ x00, x68, # cipher suites length
+ # Cipher suites (51 suites)
+ xc0, x13, xc0, x12, xc0, x11, xc0, x10,
+ xc0, x0f, xc0, x0e, xc0, x0d, xc0, x0c,
+ xc0, x0b, xc0, x0a, xc0, x09, xc0, x08,
+ xc0, x07, xc0, x06, xc0, x05, xc0, x04,
+ xc0, x03, xc0, x02, xc0, x01, x00, x39,
+ x00, x38, x00, x37, x00, x36, x00, x35, x00, x34,
+ x00, x33, x00, x32, x00, x31, x00, x30,
+ x00, x2f, x00, x16, x00, x15, x00, x14,
+ x00, x13, x00, x12, x00, x11, x00, x10,
+ x00, x0f, x00, x0e, x00, x0d, x00, x0c,
+ x00, x0b, x00, x0a, x00, x09, x00, x08,
+ x00, x07, x00, x06, x00, x05, x00, x04,
+ x00, x03, x00, x02, x00, x01, x01, x00"
+
+ fd_socket 5 || return 1
+
+# we now make a standard handshake ...
+ debugme echo -n "sending client hello... "
+ socksend "$client_hello" 1
+
+ debugme echo "reading server hello... "
+ sockread_serverhello 32768
+ if [[ $DEBUG -ge 4 ]]; then
+ hexdump -C "$SOCK_REPLY_FILE" | head -20
+ tmln_out "[...]"
+ tm_out "\nsending payload #1 with TLS version $tls_hexcode: "
+ fi
+ rm "$SOCK_REPLY_FILE"
+# ... and then send the a change cipher spec message
+ socksend "$ccs_message" 1 || ok_ids
+ sockread_serverhello 4096 $CCS_MAX_WAITSOCK
+ if [[ $DEBUG -ge 3 ]]; then
+ tmln_out "\n1st reply: "
+ hexdump -C "$SOCK_REPLY_FILE" | head -20
+ tmln_out
+ tm_out "sending payload #2 with TLS version $tls_hexcode: "
+ fi
+ rm "$SOCK_REPLY_FILE"
+
+ socksend "$ccs_message" 2 || ok_ids
+ sockread_serverhello 4096 $CCS_MAX_WAITSOCK
+ retval=$?
+
+ tls_hello_ascii=$(hexdump -v -e '16/1 "%02X"' "$SOCK_REPLY_FILE")
+ byte6="${tls_hello_ascii:12:2}"
+ debugme echo "tls_content_type: ${tls_hello_ascii:0:2} | tls_protocol: ${tls_hello_ascii:2:4} | byte6: $byte6"
+
+ if [[ $DEBUG -ge 3 ]]; then
+ tmln_out "\n2nd reply: "
+ hexdump -C "$SOCK_REPLY_FILE"
+ tmln_out
+ fi
+
+# in general, see https://en.wikipedia.org/wiki/Transport_Layer_Security#Alert_protocol
+# https://tools.ietf.org/html/rfc5246#section-7.2
+#
+# not ok for CCSI: 15 | 0301 | 00 02 | 02 15
+# ALERT | TLS 1.0 | Length=2 | Decryption failed (21)
+#
+# ok: nothing: ==> RST
+#
+# 0A: Unexpected message
+# 28: Handshake failure
+ if [[ -z "${tls_hello_ascii:0:12}" ]]; then
+ # empty reply
+ pr_svrty_best "not vulnerable (OK)"
+ if [[ $retval -eq 3 ]]; then
+ fileout "$jsonID" "OK" "not vulnerable (timed out)" "$cve" "$cwe"
+ else
+ fileout "$jsonID" "OK" "not vulnerable" "$cve" "$cwe"
+ fi
+ elif [[ "${tls_hello_ascii:0:4}" == "1503" ]]; then
+ if [[ ! "${tls_hello_ascii:5:2}" =~ [03|02|01|00] ]]; then
+ pr_warning "test failed "
+ out "no proper TLS repy (debug info: protocol sent: 1503${tls_hexcode#x03, x}, reply: ${tls_hello_ascii:0:14}"
+ fileout "$jsonID" "DEBUG" "test failed, around line $LINENO, debug info (${tls_hello_ascii:0:14})" "$cve" "$cwe" "$hint"
+ ret=1
+ elif [[ "$byte6" == "15" ]]; then
+ # decryption failed received
+ pr_svrty_critical "VULNERABLE (NOT ok)"
+ fileout "$jsonID" "CRITICAL" "VULNERABLE" "$cve" "$cwe" "$hint"
+ elif [[ "$byte6" == "0A" ]] || [[ "$byte6" == "28" ]]; then
+ # Unexpected message / Handshake failure received
+ pr_warning "likely "
+ out "not vulnerable (OK)"
+ out " - alert description type: $byte6"
+ fileout "$jsonID" "WARN" "probably not vulnerable but received 0x${byte6} instead of 0x15" "$cve" "$cwe" "$hint"
+ elif [[ "$byte6" == "14" ]]; then
+ # bad_record_mac -- this is not "not vulnerable"
+ out "likely "
+ pr_svrty_critical "VULNERABLE (NOT ok)"
+ out ", suspicious \"bad_record_mac\" ($byte6)"
+ fileout "$jsonID" "CRITICAL" "likely VULNERABLE" "$cve" "$cwe" "$hint"
+ else
+ # other errors, see https://tools.ietf.org/html/rfc5246#section-7.2
+ out "likely "
+ pr_svrty_critical "VULNERABLE (NOT ok)"
+ out ", suspicious error code \"$byte6\" returned. Please report"
+ fileout "$jsonID" "CRITICAL" "likely VULNERABLE with $byte6" "$cve" "$cwe" "$hint"
+ fi
+ elif [[ $STARTTLS_PROTOCOL == "mysql" ]] && [[ "${tls_hello_ascii:14:12}" == "233038533031" ]]; then
+ # MySQL community edition (yaSSL) returns a MySQL error instead of a TLS Alert
+ # Error: #08S01 Bad handshake
+ pr_svrty_best "not vulnerable (OK)"
+ out ", looks like MySQL community edition (yaSSL)"
+ fileout "$jsonID" "OK" "not vulnerable (MySQL community edition (yaSSL) detected)" "$cve" "$cwe"
+ elif [[ "$byte6" == [0-9a-f][0-9a-f] ]] && [[ "${tls_hello_ascii:2:2}" != "03" ]]; then
+ pr_warning "test failed"
+ out ", probably read buffer too small (${tls_hello_ascii:0:14})"
+ fileout "$jsonID" "DEBUG" "test failed, probably read buffer too small (${tls_hello_ascii:0:14})" "$cve" "$cwe" "$hint"
+ ret=1
+ else
+ pr_warning "test failed "
+ out "around line $LINENO (debug info: ${tls_hello_ascii:0:12},$byte6)"
+ fileout "$jsonID" "DEBUG" "test failed, around line $LINENO, debug info (${tls_hello_ascii:0:12},$byte6)" "$cve" "$cwe" "$hint"
+ ret=1
+ fi
+ outln
+
+ tmpfile_handle ${FUNCNAME[0]}.dd $SOCK_REPLY_FILE
+ close_socket
+ return $ret
+}
+
+sub_session_ticket_tls() {
+ local tls_proto="$1"
+ local sessticket_tls=""
+ #FIXME: we likely have done this already before (either @ run_server_defaults() or at least the output
+ # from a previous handshake) --> would save 1x connect. We have TLS_TICKET but not yet the ticket itself #FIXME
+ #ATTENTION: we DO NOT use SNI here as we assume ticketbleed is a vulnerability of the TLS stack. If we'd do SNI here, we'd also need
+ # it in the ClientHello of run_ticketbleed() otherwise the ticket will be different and the whole thing won't work!
+ #
+ sessticket_tls="$($OPENSSL s_client $(s_client_options "$BUGS $tls_proto $PROXY -connect $NODEIP:$PORT") </dev/null 2>$ERRFILE | awk '/TLS session ticket:/,/^$/' | awk '!/TLS session ticket/')"
+ sessticket_tls="$(sed -e 's/^.* - /x/g' -e 's/ .*$//g' <<< "$sessticket_tls" | tr '\n' ',')"
+ sed -e 's/ /,x/g' -e 's/-/,x/g' <<< "$sessticket_tls"
+
+}
+
+
+# see https://blog.filippo.io/finding-ticketbleed/ | https://ticketbleed.com/
+run_ticketbleed() {
+ local tls_hexcode tls_proto=""
+ local session_tckt_tls=""
+ local -i len_ch=300 # fixed len of prepared clienthello below
+ local sid="x00,x0B,xAD,xC0,xDE,x00," # some abitratry bytes
+ local len_sid="$(( ${#sid} / 4))"
+ local xlen_sid="$(dec02hex $len_sid)"
+ local -i len_tckt_tls=0 nr_sid_detected=0
+ local xlen_tckt_tls="" xlen_handshake_record_layer="" xlen_handshake_ssl_layer=""
+ local -i len_handshake_record_layer=0
+ local i
+ local -a memory sid_detected
+ local early_exit=true
+ local -i ret=0
+ local jsonID="ticketbleed"
+ local cve="CVE-2016-9244"
+ local cwe="CWE-200"
+ local hint=""
+
+ [[ -n "$STARTTLS" ]] && return 0
+ [[ $VULN_COUNT -le $VULN_THRESHLD ]] && outln && pr_headlineln " Testing for Ticketbleed vulnerability " && outln
+ pr_bold " Ticketbleed"; out " ($cve), experiment. "
+
+ if [[ "$SERVICE" != HTTP ]] && ! "$CLIENT_AUTH"; then
+ outln "-- (applicable only for HTTPS)"
+ fileout "$jsonID" "INFO" "not applicable, not HTTP" "$cve" "$cwe"
+ return 0
+ fi
+
+ # highly unlikely that it is NOT supported. We may loose time here but it's more solid
+ [[ -z "$TLS_EXTENSIONS" ]] && determine_tls_extensions
+ if [[ ! "${TLS_EXTENSIONS}" =~ "session ticket" ]]; then
+ pr_svrty_best "not vulnerable (OK)"
+ outln ", no session ticket extension"
+ fileout "$jsonID" "OK" "no session ticket extension" "$cve" "$cwe"
+ return 0
+ fi
+
+ if [[ 0 -eq $(has_server_protocol tls1) ]]; then
+ tls_hexcode="x03, x01"; tls_proto="-tls1"
+ elif [[ 0 -eq $(has_server_protocol tls1_1) ]]; then
+ tls_hexcode="x03, x02"; tls_proto="-tls1_1"
+ elif [[ 0 -eq $(has_server_protocol tls1_2) ]]; then
+ tls_hexcode="x03, x03"; tls_proto="-tls1_2"
+ elif [[ 0 -eq $(has_server_protocol ssl3) ]]; then
+ tls_hexcode="x03, x00"; tls_proto="-ssl3"
+ else # no protocol for some reason defined, determine TLS versions offered with a new handshake
+ "$HAS_TLS13" && tls_proto="-no_tls1_3"
+ $OPENSSL s_client $(s_client_options "$STARTTLS $BUGS $tls_proto -connect $NODEIP:$PORT $PROXY") >$TMPFILE 2>$ERRFILE </dev/null
+ case "$(get_protocol $TMPFILE)" in
+ *1.2) tls_hexcode="x03, x03"; tls_proto="-tls1_2" ; add_tls_offered tls1_2 yes ;;
+ *1.1) tls_hexcode="x03, x02"; tls_proto="-tls1_1" ; add_tls_offered tls1_1 yes ;;
+ TLSv1) tls_hexcode="x03, x01"; tls_proto="-tls1" ; add_tls_offered tls1 yes ;;
+ SSLv3) tls_hexcode="x03, x00"; tls_proto="-ssl3" ; add_tls_offered ssl3 yes ;;
+ esac
+ fi
+ debugme echo "using protocol $tls_hexcode"
+
+ session_tckt_tls="$(sub_session_ticket_tls "$tls_proto")"
+ if [[ "$session_tckt_tls" == "," ]]; then
+ pr_svrty_best "not vulnerable (OK)"
+ outln ", no session tickets"
+ fileout "$jsonID" "OK" "not vulnerable" "$cve" "$cwe"
+ debugme echo " session ticket TLS \"$session_tckt_tls\""
+ return 0
+ fi
+
+ len_tckt_tls=${#session_tckt_tls}
+ len_tckt_tls=$(( len_tckt_tls / 4))
+ xlen_tckt_tls="$(dec02hex $len_tckt_tls)"
+ len_handshake_record_layer="$(( len_sid + len_ch + len_tckt_tls ))"
+ xlen_handshake_record_layer="$(dec04hex "$len_handshake_record_layer")"
+ len_handshake_ssl_layer="$(( len_handshake_record_layer + 4 ))"
+ xlen_handshake_ssl_layer="$(dec04hex "$len_handshake_ssl_layer")"
+
+ if [[ "$DEBUG" -ge 4 ]]; then
+ echo "len_tckt_tls (hex): $len_tckt_tls ($xlen_tckt_tls)"
+ echo "sid: $sid"
+ echo "len_sid (hex) $len_sid ($xlen_sid)"
+ echo "len_handshake_record_layer: $len_handshake_record_layer ($xlen_handshake_record_layer)"
+ echo "len_handshake_ssl_layer: $len_handshake_ssl_layer ($xlen_handshake_ssl_layer)"
+ echo "session_tckt_tls: $session_tckt_tls"
+ fi
+
+ client_hello="
+ # TLS header (5 bytes)
+ ,x16, # Content type (x16 for handshake)
+ x03,x01, # TLS version record layer
+ # Length Secure Socket Layer follows:
+ $xlen_handshake_ssl_layer,
+ # Handshake header
+ x01, # Type (x01 for ClientHello)
+ # Length of ClientHello follows:
+ x00, $xlen_handshake_record_layer,
+ $tls_hexcode, # TLS Version
+ # Random (32 byte) Unix time etc, see www.moserware.com/2009/06/first-few-milliseconds-of-https.html
+ xee, xee, x5b, x90, x9d, x9b, x72, x0b,
+ xbc, x0c, xbc, x2b, x92, xa8, x48, x97,
+ xcf, xbd, x39, x04, xcc, x16, x0b, x85,
+ x03, x90, x9f, x77, x04, x33, xff, xff,
+ $xlen_sid, # Session ID length
+ $sid
+ x00, x6a, # Cipher suites length 106
+ # 53 Cipher suites
+ xc0,x14, xc0,x13, xc0,x0a, xc0,x21,
+ x00,x39, x00,x38, x00,x88, x00,x87,
+ xc0,x0f, xc0,x05, x00,x35, x00,x84,
+ xc0,x12, xc0,x08, xc0,x1c, xc0,x1b,
+ x00,x16, x00,x13, xc0,x0d, xc0,x03,
+ x00,x0a, xc0,x13, xc0,x09, xc0,x1f,
+ xc0,x1e, x00,x33, x00,x32, x00,x9a,
+ x00,x99, x00,x45, x00,x44, xc0,x0e,
+ xc0,x04, x00,x2f, x00,x96, x00,x41,
+ xc0,x11, xc0,x07, xc0,x0c, xc0,x02,
+ x00,x05, x00,x04, x00,x15, x00,x12,
+ xc0,x30, xc0,x2f, x00,x9d, x00,x9c,
+ x00,x3d, x00,x3c, x00,x9f, x00,x9e,
+ x00,xff,
+ x01, # Compression methods length
+ x00, # Compression method (x00 for NULL)
+ x01,x5b, # Extensions length ####### 10b + x14 + x3c
+# Extension Padding
+ x00,x15,
+ # length:
+ x00,x38,
+ x00,x00, x00,x00, x00,x00, x00,x00, x00,x00, x00,x00, x00,x00, x00,x00, x00,x00, x00,x00,
+ x00,x00, x00,x00, x00,x00, x00,x00, x00,x00, x00,x00, x00,x00, x00,x00, x00,x00, x00,x00,
+ x00,x00, x00,x00, x00,x00, x00,x00, x00,x00, x00,x00, x00,x00, x00,x00,
+# Extension: ec_point_formats
+ x00,x0b,
+ # length:
+ x00,x04,
+ # data:
+ x03,x00, x01,x02,
+# Extension: elliptic_curves
+ x00,x0a,
+ # length
+ x00,x34,
+ x00,x32,
+ # data:
+ x00,x0e, x00,x0d, x00,x19, x00,x0b, x00,x0c,
+ x00,x18, x00,x09, x00,x0a, x00,x16,
+ x00,x17, x00,x08, x00,x06, x00,x07,
+ x00,x14, x00,x15, x00,x04, x00,x05,
+ x00,x12, x00,x13, x00,x01, x00,x02,
+ x00,x03, x00,x0f, x00,x10, x00,x11,
+# Extension: Signature Algorithms
+ x00,x0d,
+ # length:
+ x00,x10,
+ # data:
+ x00,x0e ,x04,x01, x05,x01 ,x02,x01, x04,x03, x05,x03,
+ x02,x03, x02,x02,
+# Extension: SessionTicket TLS
+ x00, x23,
+ # length of SessionTicket TLS
+ x00, $xlen_tckt_tls,
+ # data, Session Ticket
+ $session_tckt_tls # here we have the comma already
+# Extension: Heartbeat
+ x00, x0f, x00, x01, x01"
+
+ # we do 3 client hellos, then see whether different memory is returned
+ for i in 1 2 3; do
+ fd_socket 5 || return 6
+ debugme echo -n "sending client hello... "
+ socksend "$client_hello" 0
+
+ debugme echo "reading server hello (ticketbleed reply)... "
+ if "$FAST_SOCKET"; then
+ tls_hello_ascii=$(sockread_fast 32768)
+ else
+ sockread_serverhello 32768 $CCS_MAX_WAITSOCK
+ tls_hello_ascii=$(hexdump -v -e '16/1 "%02X"' "$SOCK_REPLY_FILE")
+ fi
+ [[ "$DEBUG" -ge 5 ]] && echo "$tls_hello_ascii"
+ if [[ "$DEBUG" -ge 4 ]]; then
+ echo "============================="
+ echo "$tls_hello_ascii"
+ echo "============================="
+ fi
+
+ if [[ "${tls_hello_ascii:0:2}" == 15 ]]; then
+ debugme echo -n "TLS Alert ${tls_hello_ascii:10:4} (TLS version: ${tls_hello_ascii:2:4}) -- "
+ pr_svrty_best "not vulnerable (OK)"
+ fileout "$jsonID" "OK" "not vulnerable" "$cve" "$cwe"
+ send_close_notify "${tls_hello_ascii:18:4}"
+ close_socket
+ break
+ elif [[ -z "${tls_hello_ascii:0:2}" ]]; then
+ pr_svrty_best "not vulnerable (OK)"
+ out ", reply empty"
+ fileout "$jsonID" "OK" "not vulnerable" "$cve" "$cwe"
+ send_close_notify "${tls_hello_ascii:18:4}"
+ close_socket
+ break
+ elif [[ "${tls_hello_ascii:0:2}" == 16 ]]; then
+ early_exit=false
+ debugme echo -n "Handshake (TLS version: ${tls_hello_ascii:2:4}), "
+ if [[ "${tls_hello_ascii:10:6}" == 020000 ]]; then
+ debugme echo -n "ServerHello -- "
+ else
+ debugme echo -n "Message type: ${tls_hello_ascii:10:6} -- "
+ fi
+ sid_input=$(sed -e 's/x//g' -e 's/,//g' <<< "$sid")
+ sid_detected[i]="${tls_hello_ascii:88:32}"
+ memory[i]="${tls_hello_ascii:$((88+ len_sid*2)):$((32 - len_sid*2))}"
+ if [[ "$DEBUG" -ge 3 ]]; then
+ echo
+ echo "TLS version, record layer: ${tls_hello_ascii:18:4}"
+ echo "Session ID: ${sid_detected[i]}"
+ echo "memory: ${memory[i]}"
+ echo -n "$sid_input in SID: " ;
+ [[ "${sid_detected[i]}" =~ $sid_input ]] && echo "yes" || echo "no"
+ fi
+ [[ "$DEBUG" -ge 1 ]] && echo $tls_hello_ascii >$TEMPDIR/${FUNCNAME[0]}.tls_hello_ascii${i}.txt
+ else
+ ret=1
+ pr_warning "test failed"
+ out " around line $LINENO (debug info: ${tls_hello_ascii:0:2}, ${tls_hello_ascii:2:10})"
+ fileout "$jsonID" "DEBUG" "test failed, around $LINENO (debug info: ${tls_hello_ascii:0:2}, ${tls_hello_ascii:2:10})" "$cve" "$cwe"
+ send_close_notify "${tls_hello_ascii:18:4}"
+ close_socket
+ break
+ fi
+ send_close_notify "${tls_hello_ascii:18:4}"
+ close_socket
+ done
+
+ if ! "$early_exit"; then
+ # here we test the replies if a TLS server hello was received >1x
+ for i in 1 2 3 ; do
+ if [[ "${sid_detected[i]}" =~ $sid_input ]]; then
+ # was our faked TLS SID returned?
+ nr_sid_detected+=1
+ fi
+ done
+ if [[ $nr_sid_detected -eq 3 ]]; then
+ if [[ ${memory[1]} != ${memory[2]} ]] && [[ ${memory[2]} != ${memory[3]} ]]; then
+ pr_svrty_critical "VULNERABLE (NOT ok)"
+ fileout "$jsonID" "CRITICAL" "VULNERABLE" "$cve" "$cwe" "$hint"
+ else
+ pr_svrty_best "not vulnerable (OK)"
+ out ", session IDs were returned but potential memory fragments do not differ"
+ fileout "$jsonID" "OK" "not vulnerable, returned potential memory fragments do not differ" "$cve" "$cwe"
+ fi
+ else
+ if [[ "$DEBUG" -ge 2 ]]; then
+ echo
+ pr_warning "test failed, non reproducible results!"
+ else
+ pr_warning "test failed, non reproducible results!"
+ out " Please run again w \"--debug=2\" (# of faked TLS SIDs detected: $nr_sid_detected)"
+ fi
+ fileout "$jsonID" "DEBUG" "test failed, non reproducible results. $nr_sid_detected TLS Session IDs $nr_sid_detected, ${sid_detected[1]},${sid_detected[2]},${sid_detected[3]}" "$cve" "$cwe"
+ ret=1
+ fi
+ fi
+ outln
+ return $ret
+}
+
+# Overview @ https://www.exploresecurity.com/wp-content/uploads/custom/SSL_manual_cheatsheet.html
+#
+run_renego() {
+ local legacycmd="" proto="$OPTIMAL_PROTO"
+ local sec_renego sec_client_renego
+ local -i ret=0
+ local cve=""
+ local cwe="CWE-310"
+ local hint=""
+ local jsonID=""
+ # No SNI needed here as there won't be two different SSL stacks for one IP
+
+ "$HAS_TLS13" && [[ -z "$proto" ]] && proto="-no_tls1_3"
+
+ [[ $VULN_COUNT -le $VULN_THRESHLD ]] && outln && pr_headlineln " Testing for Renegotiation vulnerabilities " && outln
+
+ pr_bold " Secure Renegotiation (RFC 5746) "
+ jsonID="secure_renego"
+
+ if "$TLS13_ONLY"; then
+ # https://www.openssl.org/blog/blog/2018/02/08/tlsv1.3/
+ pr_svrty_best "not vulnerable (OK)"
+ [[ $DEBUG -ge 1 ]] && out ", no renegotiation support in TLS 1.3 only servers"
+ outln
+ fileout "$jsonID" "OK" "TLS 1.3 only server" "$cve" "$cwe"
+ else
+ # first fingerprint for the Line "Secure Renegotiation IS NOT" or "Secure Renegotiation IS "
+ $OPENSSL s_client $(s_client_options "$proto $STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY") 2>&1 </dev/null >$TMPFILE 2>$ERRFILE
+ if sclient_connect_successful $? $TMPFILE; then
+ grep -iaq "Secure Renegotiation IS NOT" $TMPFILE
+ sec_renego=$? # 0= Secure Renegotiation IS NOT supported
+ # grep -iaq "Secure Renegotiation IS supported"
+ #FIXME: didn't occur to me yet but why not also to check on "Secure Renegotiation IS supported"
+ case $sec_renego in
+ 0) prln_svrty_critical "Not supported / VULNERABLE (NOT ok)"
+ fileout "$jsonID" "CRITICAL" "VULNERABLE" "$cve" "$cwe" "$hint"
+ ;;
+ 1) prln_svrty_best "supported (OK)"
+ fileout "$jsonID" "OK" "supported" "$cve" "$cwe"
+ ;;
+ *) prln_warning "FIXME (bug): $sec_renego"
+ fileout "$jsonID" "WARN" "FIXME (bug) $sec_renego" "$cve" "$cwe"
+ ;;
+ esac
+ else
+ prln_warning "OpenSSL handshake didn't succeed"
+ fileout "$jsonID" "WARN" "OpenSSL handshake didn't succeed" "$cve" "$cwe"
+ fi
+ fi
+
+ # FIXME: Basically this can be done with sockets and we might have that information already
+ # see https://tools.ietf.org/html/rfc5746#section-3.4: 'The client MUST include either an empty "renegotiation_info"
+ # extension, or the TLS_EMPTY_RENEGOTIATION_INFO_SCSV signaling cipher suite value in the ClientHello. [..]
+ # When a ServerHello is received, the client MUST check if it includes the "renegotiation_info" extension:
+ # If the extension is not present, the server does not support secure renegotiation'
+
+
+ pr_bold " Secure Client-Initiated Renegotiation "
+ jsonID="secure_client_renego"
+ cve="CVE-2011-1473"
+ # see: https://blog.qualys.com/ssllabs/2011/10/31/tls-renegotiation-and-denial-of-service-attacks
+ # https://blog.ivanristic.com/2009/12/testing-for-ssl-renegotiation.html -- head/get doesn't seem to be needed though
+ # https://archive.fo/20130415224936/http://www.thc.org/thc-ssl-dos/
+ # https://vincent.bernat.ch/en/blog/2011-ssl-dos-mitigation
+ case "$OSSL_VER" in
+ 0.9.8*) # we need this for Mac OSX unfortunately
+ case "$OSSL_VER_APPENDIX" in
+ [a-l])
+ prln_local_problem " Your $OPENSSL cannot test this secure renegotiation vulnerability"
+ fileout "$jsonID" "WARN" "your $OPENSSL cannot test this secure renegotiation vulnerability" "$cve" "$cwe"
+ return 1
+ ;;
+ [m-z])
+ ;; # all ok
+ esac
+ ;;
+ 1.0.1*|1.0.2*)
+ legacycmd="-legacy_renegotiation"
+ ;;
+ 0.9.9*|1.0*|1.1*)
+ ;; # all ok
+ esac
+
+ if "$TLS13_ONLY"; then
+ pr_svrty_best "not vulnerable (OK)"
+ [[ $DEBUG -ge 1 ]] && out ", no renegotiation support in TLS 1.3 only servers"
+ outln
+ fileout "$jsonID" "OK" "not vulnerable, TLS 1.3 only" "$cve" "$cwe"
+ elif "$CLIENT_AUTH"; then
+ prln_warning "client x509-based authentication prevents this from being tested"
+ fileout "$jsonID" "WARN" "client x509-based authentication prevents this from being tested"
+ sec_client_renego=1
+ else
+ # We need up to two tries here, as some LiteSpeed servers don't answer on "R" and block. Thus first try in the background
+ # msg enables us to look deeper into it while debugging
+ echo R | $OPENSSL s_client $(s_client_options "$proto $BUGS $legacycmd $STARTTLS -connect $NODEIP:$PORT $PROXY") >$TMPFILE 2>>$ERRFILE &
+ wait_kill $! $HEADER_MAXSLEEP
+ if [[ $? -eq 3 ]]; then
+ pr_svrty_good "likely not vulnerable (OK)"; outln ", timed out" # it hung
+ fileout "$jsonID" "OK" "likely not vulnerable (timed out)" "$cve" "$cwe"
+ sec_client_renego=1
+ else
+ # second try in the foreground as we are sure now it won't hang
+ echo R | $OPENSSL s_client $(s_client_options "$proto $legacycmd $STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY") >$TMPFILE 2>>$ERRFILE
+ sec_client_renego=$?
+ # 0 means client is renegotiating & doesn't return an error --> vuln!
+ # 1 means client tried to renegotiating but the server side errored then. You still see RENEGOTIATING in the output
+ if tail -5 $TMPFILE| grep -qa '^closed'; then
+ # Exemption from above: server closed the connection but return value was zero
+ # See https://github.com/drwetter/testssl.sh/issues/1725 and referenced issue @haproxy
+ sec_client_renego=1
+ fi
+ case "$sec_client_renego" in
+ 0) # We try again if server is HTTP. This could be either a node.js server or something else.
+ # node.js has a mitigation which allows 3x R and then blocks. So we test 4x
+ # This way we save a couple seconds as we weeded out the ones which are more robust
+ if [[ $SERVICE != HTTP ]]; then
+ pr_svrty_medium "VULNERABLE (NOT ok)"; outln ", potential DoS threat"
+ fileout "$jsonID" "MEDIUM" "VULNERABLE, potential DoS threat" "$cve" "$cwe" "$hint"
+ else
+ (for i in {1..4}; do echo R; sleep 1; done) | \
+ $OPENSSL s_client $(s_client_options "$proto $legacycmd $STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY") >$TMPFILE 2>>$ERRFILE
+ case $? in
+ 0) pr_svrty_high "VULNERABLE (NOT ok)"; outln ", DoS threat"
+ fileout "$jsonID" "HIGH" "VULNERABLE, DoS threat" "$cve" "$cwe" "$hint"
+ ;;
+ 1) pr_svrty_good "not vulnerable (OK)"
+ outln " -- mitigated"
+ fileout "$jsonID" "OK" "not vulnerable, mitigated" "$cve" "$cwe"
+ ;;
+ *) prln_warning "FIXME (bug): $sec_client_renego (4 tries)"
+ fileout "$jsonID" "DEBUG" "FIXME (bug 4 tries) $sec_client_renego" "$cve" "$cwe"
+ ret=1
+ ;;
+ esac
+ fi
+ ;;
+ 1)
+ prln_svrty_good "not vulnerable (OK)"
+ fileout "$jsonID" "OK" "not vulnerable" "$cve" "$cwe"
+ ;;
+ *)
+ prln_warning "FIXME (bug): $sec_client_renego"
+ fileout "$jsonID" "DEBUG" "FIXME (bug) $sec_client_renego - Please report" "$cve" "$cwe"
+ ret=1
+ ;;
+ esac
+ fi
+ fi
+
+ #pr_bold " Insecure Client-Initiated Renegotiation " # pre-RFC 5746, CVE-2009-3555
+ #jsonID="insecure_client_renego"
+ #
+ # https://www.openssl.org/news/vulnerabilities.html#y2009. It can only be tested with OpenSSL <=0.9.8k
+ # Insecure Client-Initiated Renegotiation is missing ==> sockets. When we complete the handshake ;-)
+
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return $ret
+}
+
+run_crime() {
+ local -i ret=0 sclient_success
+ local addcmd=""
+ local cve="CVE-2012-4929"
+ local cwe="CWE-310"
+ local hint=""
+
+ # In a nutshell: don't offer TLS/SPDY compression. This tests for CRIME Vulnerability on HTTPS only,
+ # not SPDY or ALPN (yet). Please note that it is an attack where you need client side control, so in
+ # regular situations this # means anyway "game over", with or without CRIME.
+ #
+ # https://blog.qualys.com/ssllabs/2012/09/14/crime-information-leakage-attack-against-ssltls
+
+ [[ $VULN_COUNT -le $VULN_THRESHLD ]] && outln && pr_headlineln " Testing for CRIME vulnerability " && outln
+ pr_bold " CRIME, TLS " ; out "($cve) "
+
+ if "$TLS13_ONLY"; then
+ pr_svrty_best "not vulnerable (OK)"
+ [[ $DEBUG -ge 1 ]] && out ", no compression in TLS 1.3 only servers"
+ outln
+ fileout "$jsonID" "OK" "TLS 1.3 only server" "$cve" "$cwe"
+ return 0
+ fi
+
+ if ! "$HAS_ZLIB"; then
+ if "$SSL_NATIVE"; then
+ prln_local_problem "$OPENSSL lacks zlib support"
+ fileout "CRIME_TLS" "WARN" "CRIME, TLS: Not tested. $OPENSSL lacks zlib support" "$cve" "$cwe"
+ return 1
+ else
+ tls_sockets "03" "$TLS12_CIPHER" "" "" "true"
+ sclient_success=$?
+ [[ $sclient_success -eq 2 ]] && sclient_success=0
+ [[ $sclient_success -eq 0 ]] && cp "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt" $TMPFILE
+ fi
+ else
+ [[ "$OSSL_VER" == 0.9.8* ]] && addcmd="-no_ssl2"
+ "$HAS_TLS13" && [[ -z "$OPTIMAL_PROTO" ]] && addcmd+=" -no_tls1_3"
+ $OPENSSL s_client $(s_client_options "$OPTIMAL_PROTO $BUGS -comp $addcmd $STARTTLS -connect $NODEIP:$PORT $PROXY $SNI") </dev/null &>$TMPFILE
+ sclient_connect_successful $? $TMPFILE
+ sclient_success=$?
+ fi
+
+ if [[ $sclient_success -ne 0 ]]; then
+ pr_warning "test failed (couldn't connect)"
+ fileout "CRIME_TLS" "WARN" "Check failed, couldn't connect" "$cve" "$cwe"
+ ret=1
+ elif grep -a Compression $TMPFILE | grep -aq NONE >/dev/null; then
+ pr_svrty_good "not vulnerable (OK)"
+ if [[ $SERVICE != HTTP ]] && ! "$CLIENT_AUTH"; then
+ out " (not using HTTP anyway)"
+ fileout "CRIME_TLS" "OK" "not vulnerable (not using HTTP anyway)" "$cve" "$cwe"
+ else
+ fileout "CRIME_TLS" "OK" "not vulnerable" "$cve" "$cwe"
+ fi
+ else
+ if [[ $SERVICE == HTTP ]] || "$CLIENT_AUTH"; then
+ pr_svrty_high "VULNERABLE (NOT ok)"
+ fileout "CRIME_TLS" "HIGH" "VULNERABLE" "$cve" "$cwe" "$hint"
+ else
+ pr_svrty_medium "VULNERABLE but not using HTTP: probably no exploit known"
+ fileout "CRIME_TLS" "MEDIUM" "VULNERABLE, but not using HTTP. Probably no exploit known" "$cve" "$cwe" "$hint"
+ # not clear whether a protocol != HTTP offers the ability to repeatedly modify the input
+ # which is done e.g. via javascript in the context of HTTP
+ fi
+ fi
+ outln
+
+# this needs to be re-done i order to remove the redundant check for spdy
+
+ # weed out starttls, spdy-crime is a web thingy
+# if [[ "x$STARTTLS" != "x" ]]; then
+# echo
+# return $ret
+# fi
+
+ # weed out non-webports, spdy-crime is a web thingy. there's a catch thoug, you see it?
+# case $PORT in
+# 25|465|587|80|110|143|993|995|21)
+# echo
+# return $ret
+# esac
+
+# if "$HAS_NPN"; then
+# $OPENSSL s_client -host $NODE -port $PORT -nextprotoneg $NPN_PROTOs $SNI </dev/null 2>/dev/null >$TMPFILE
+# if [[ $? -eq 0 ]]; then
+# echo
+# pr_bold "CRIME Vulnerability, SPDY " ; outln "($cve): "
+
+# STR=$(grep Compression $TMPFILE )
+# if echo $STR | grep -q NONE >/dev/null; then
+# pr_svrty_best "not vulnerable (OK)"
+# ret=$((ret + 0))
+# else
+# pr_svrty_critical "VULNERABLE (NOT ok)"
+# ret=$((ret + 1))
+# fi
+# fi
+# fi
+# [[ $DEBUG -ge 2 ]] tmln_out "$STR"
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return $ret
+}
+
+
+# BREACH is a HTTP-level compression & an attack which works against any cipher suite and is agnostic
+# to the version of TLS/SSL, more: http://www.breachattack.com/ . Foreign referrers are the important thing here!
+# Mitigation: see https://community.qualys.com/message/20360
+#
+run_breach() {
+ local header
+ local -i ret=0
+ local -i was_killed=0
+ local referer useragent
+ local url="$1"
+ local spaces=" "
+ local disclaimer=""
+ local when_makesense=" Can be ignored for static pages or if no secrets in the page"
+ local cve="CVE-2013-3587"
+ local cwe="CWE-310"
+ local hint=""
+ local jsonID="BREACH"
+
+ [[ $SERVICE != HTTP ]] && ! "$CLIENT_AUTH" && return 7
+
+ [[ $VULN_COUNT -le $VULN_THRESHLD ]] && outln && pr_headlineln " Testing for BREACH (HTTP compression) vulnerability " && outln
+ pr_bold " BREACH"; out " ($cve) "
+ if "$CLIENT_AUTH"; then
+ prln_warning "client x509-based authentication prevents this from being tested"
+ fileout "$jsonID" "WARN" "client x509-based authentication prevents this from being tested" "$cve" "$cwe"
+ return 7
+ fi
+
+ # if [[ $NR_HEADER_FAIL -ge $MAX_HEADER_FAIL ]]; then
+ # pr_warning "Retrieving HTTP header failed before. Skipping."
+ # fileout "$jsonID" "WARN" "HTTP response was wampty before" "$cve" "$cwe"
+ # outln
+ # return 1
+ # fi
+
+ [[ -z "$url" ]] && url="/"
+ disclaimer=" - only supplied \"$url\" tested"
+
+ referer="https://google.com/"
+ [[ "$NODE" =~ google ]] && referer="https://yandex.ru/" # otherwise we have a false positive for google.com
+ useragent="$UA_STD"
+ $SNEAKY && useragent="$UA_SNEAKY"
+ printf "GET $url HTTP/1.1\r\nHost: $NODE\r\nUser-Agent: $useragent\r\nReferer: $referer\r\nConnection: Close\r\nAccept-encoding: gzip,deflate,compress,br\r\nAccept: text/*\r\n\r\n" | $OPENSSL s_client $(s_client_options "$OPTIMAL_PROTO $BUGS -quiet -ign_eof -connect $NODEIP:$PORT $PROXY $SNI") 1>$TMPFILE 2>$ERRFILE &
+ wait_kill $! $HEADER_MAXSLEEP
+ was_killed=$? # !=0 was killed
+ result="$(grep -ia Content-Encoding: $TMPFILE)"
+ result="$(strip_lf "$result")"
+ result="${result#*:}"
+ result="$(strip_spaces "$result")"
+ debugme echo "$result"
+ if [[ ! -s $TMPFILE ]]; then
+ pr_warning "failed (HTTP header request stalled or empty return"
+ if [[ $was_killed -ne 0 ]]; then
+ pr_warning " and was terminated"
+ fileout "$jsonID" "WARN" "Test failed as HTTP request stalled and was terminated" "$cve" "$cwe"
+ else
+ fileout "$jsonID" "WARN" "Test failed as HTTP response was empty" "$cve" "$cwe"
+ fi
+ prln_warning ") "
+ ret=1
+ elif [[ -z $result ]]; then
+ pr_svrty_good "no HTTP compression (OK) "
+ outln "$disclaimer"
+ fileout "$jsonID" "OK" "not vulnerable, no HTTP compression $disclaimer" "$cve" "$cwe"
+ else
+ pr_svrty_medium "potentially NOT ok, \"$result\" HTTP compression detected."
+ outln "$disclaimer"
+ outln "$spaces$when_makesense"
+ fileout "$jsonID" "MEDIUM" "potentially VULNERABLE, $result HTTP compression detected $disclaimer" "$cve" "$cwe" "$hint"
+ fi
+ # Any URL can be vulnerable. I am testing now only the given URL!
+
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return $ret
+}
+
+
+# SWEET32 (https://sweet32.info/). Birthday attacks on 64-bit block ciphers.
+# In a nutshell: don't use 3DES ciphers anymore (DES, RC2 and IDEA too).
+# Please note as opposed to RC4 (stream cipher) RC2 is a block cipher.
+#
+run_sweet32() {
+ local -i sclient_success=1
+ local sweet32_ciphers="IDEA-CBC-SHA:IDEA-CBC-MD5:RC2-CBC-MD5:KRB5-IDEA-CBC-SHA:KRB5-IDEA-CBC-MD5:ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-SHA:SRP-DSS-3DES-EDE-CBC-SHA:SRP-RSA-3DES-EDE-CBC-SHA:SRP-3DES-EDE-CBC-SHA:EDH-RSA-DES-CBC3-SHA:EDH-DSS-DES-CBC3-SHA:DH-RSA-DES-CBC3-SHA:DH-DSS-DES-CBC3-SHA:AECDH-DES-CBC3-SHA:ADH-DES-CBC3-SHA:ECDH-RSA-DES-CBC3-SHA:ECDH-ECDSA-DES-CBC3-SHA:DES-CBC3-SHA:DES-CBC3-MD5:DES-CBC3-SHA:RSA-PSK-3DES-EDE-CBC-SHA:PSK-3DES-EDE-CBC-SHA:KRB5-DES-CBC3-SHA:KRB5-DES-CBC3-MD5:ECDHE-PSK-3DES-EDE-CBC-SHA:DHE-PSK-3DES-EDE-CBC-SHA:DES-CFB-M1:EXP1024-DHE-DSS-DES-CBC-SHA:EDH-RSA-DES-CBC-SHA:EDH-DSS-DES-CBC-SHA:DH-RSA-DES-CBC-SHA:DH-DSS-DES-CBC-SHA:ADH-DES-CBC-SHA:EXP1024-DES-CBC-SHA:DES-CBC-SHA:EXP1024-RC2-CBC-MD5:DES-CBC-MD5:DES-CBC-SHA:KRB5-DES-CBC-SHA:KRB5-DES-CBC-MD5:EXP-EDH-RSA-DES-CBC-SHA:EXP-EDH-DSS-DES-CBC-SHA:EXP-ADH-DES-CBC-SHA:EXP-DES-CBC-SHA:EXP-RC2-CBC-MD5:EXP-RC2-CBC-MD5:EXP-KRB5-RC2-CBC-SHA:EXP-KRB5-DES-CBC-SHA:EXP-KRB5-RC2-CBC-MD5:EXP-KRB5-DES-CBC-MD5:EXP-DH-DSS-DES-CBC-SHA:EXP-DH-RSA-DES-CBC-SHA"
+ local sweet32_ciphers_hex="00,07, 00,21, 00,25, c0,12, c0,08, c0,1c, c0,1b, c0,1a, 00,16, 00,13, 00,10, 00,0d, c0,17, 00,1b, c0,0d, c0,03, 00,0a, 00,93, 00,8b, 00,1f, 00,23, c0,34, 00,8f, fe,ff, ff,e0, 00,63, 00,15, 00,12, 00,0f, 00,0c, 00,1a, 00,62, 00,09, 00,61, 00,1e, 00,22, fe,fe, ff,e1, 00,14, 00,11, 00,19, 00,08, 00,06, 00,27, 00,26, 00,2a, 00,29, 00,0b, 00,0e"
+ local ssl2_sweet32_ciphers='RC2-CBC-MD5:EXP-RC2-CBC-MD5:IDEA-CBC-MD5:DES-CBC-MD5:DES-CBC-SHA:DES-CBC3-MD5:DES-CBC3-SHA:DES-CFB-M1'
+ local ssl2_sweet32_ciphers_hex='03,00,80, 04,00,80, 05,00,80, 06,00,40, 06,01,40, 07,00,C0, 07,01,C0, FF,80,00'
+ local nr_cipher_minimal=21
+ local proto
+ local cve="CVE-2016-2183 CVE-2016-6329"
+ local cwe="CWE-327"
+ local hint=""
+ local -i nr_sweet32_ciphers=0 nr_supported_ciphers=0 nr_ssl2_sweet32_ciphers=0 nr_ssl2_supported_ciphers=0
+ local ssl2_sweet=false
+ local using_sockets=true
+
+ [[ $VULN_COUNT -le $VULN_THRESHLD ]] && outln && pr_headlineln " Testing for SWEET32 (Birthday Attacks on 64-bit Block Ciphers) " && outln
+ pr_bold " SWEET32"; out " (${cve// /, }) "
+
+ if "$TLS13_ONLY"; then
+ # Unfortunately there's no restriction using TLS 1.2 with $sweet32_ciphers
+ pr_svrty_best "not vulnerable (OK)"
+ [[ $DEBUG -ge 1 ]] && out ", TLS 1.3 doesn't offer such ciphers"
+ outln
+ fileout "$jsonID" "OK" "not vulnerable" "$cve" "$cwe"
+ return 0
+ fi
+
+ "$SSL_NATIVE" && using_sockets=false
+ # The openssl binary distributed has almost everything we need (PSK, KRB5 ciphers and feff, ffe0 are typically missing).
+ # Measurements show that there's little impact whether we use sockets or TLS here, so the default is sockets here.
+ if "$using_sockets"; then
+ for proto in 03 02 01 00; do
+ [[ $(has_server_protocol "$proto") -eq 1 ]] && continue
+ tls_sockets "$proto" "${sweet32_ciphers_hex}, 00,ff"
+ sclient_success=$?
+ [[ $sclient_success -eq 2 ]] && sclient_success=0
+ [[ $sclient_success -eq 0 ]] && break
+ done
+ if [[ 1 -ne $(has_server_protocol "ssl2") ]]; then
+ sslv2_sockets "$ssl2_sweet32_ciphers_hex"
+ case $? in
+ 3) ssl2_sweet=true
+ add_tls_offered ssl2 yes ;;
+ 0) ;; # ssl2_sweet=false
+ 1|4|6|7) debugme "${FUNCNAME[0]}: test problem we don't handle here"
+ ;;
+ esac
+ fi
+ else
+ nr_sweet32_ciphers=$(count_ciphers $sweet32_ciphers)
+ nr_supported_ciphers=$(count_ciphers $(actually_supported_osslciphers $sweet32_ciphers))
+ debugme echo "$nr_sweet32_ciphers / $nr_supported_ciphers"
+
+ nr_ssl2_sweet32_ciphers=$(count_ciphers $ssl2_sweet32_ciphers)
+ nr_ssl2_supported_ciphers=$(count_ciphers $(actually_supported_osslciphers $ssl2_sweet32_ciphers))
+ debugme echo "$nr_ssl2_sweet32_ciphers / $nr_ssl2_supported_ciphers"
+
+ if [[ $(( nr_supported_ciphers + nr_ssl2_supported_ciphers )) -le $nr_cipher_minimal ]]; then
+ pr_local_problem "Only ${nr_supported_ciphers}+${nr_ssl2_supported_ciphers} \"SWEET32 ciphers\" found in your $OPENSSL."
+ outln " Test skipped"
+ fileout "SWEET32" "WARN" "Not tested, lack of local support ($((nr_supported_ciphers + nr_ssl2_supported_ciphers)) ciphers only)" "$cve" "$cwe" "$hint"
+ return 1
+ fi
+ for proto in -no_ssl2 -tls1_1 -tls1 -ssl3; do
+ [[ $nr_supported_ciphers -eq 0 ]] && break
+ ! "$HAS_SSL3" && [[ "$proto" == -ssl3 ]] && continue
+ if [[ "$proto" != -no_ssl2 ]]; then
+ "$FAST" && break
+ [[ $(has_server_protocol "${proto:1}") -eq 1 ]] && continue
+ fi
+ $OPENSSL s_client $(s_client_options "$STARTTLS $BUGS $proto -cipher $sweet32_ciphers -connect $NODEIP:$PORT $PROXY $SNI") >$TMPFILE 2>$ERRFILE </dev/null
+ sclient_connect_successful $? $TMPFILE
+ sclient_success=$?
+ [[ $DEBUG -ge 2 ]] && grep -Eq "error|failure" $ERRFILE | grep -Eav "unable to get local|verify error"
+ [[ $sclient_success -eq 0 ]] && break
+ done
+ if "$HAS_SSL2"; then
+ if [[ 1 -ne $(has_server_protocol "ssl2") ]]; then
+ $OPENSSL s_client $STARTTLS $BUGS -ssl2 -cipher $ssl2_sweet32_ciphers -connect $NODEIP:$PORT $PROXY >$TMPFILE 2>$ERRFILE </dev/null
+ sclient_connect_successful $? $TMPFILE
+ if [[ $? -eq 0 ]]; then
+ ssl2_sweet=true
+ add_tls_offered ssl2 yes
+ fi
+ fi
+ else
+ debugme tm_warning "Can't test with SSLv2 here as $OPENSSL lacks support"
+ # we omit adding a string for DEBUG==0 here as using sockets is the default and the following elif statement becomes ugly
+ fi
+ fi
+ if [[ $sclient_success -eq 0 ]] && "$ssl2_sweet" ; then
+ pr_svrty_low "VULNERABLE"; out ", uses 64 bit block ciphers for SSLv2 and above"
+ fileout "SWEET32" "LOW" "uses 64 bit block ciphers for SSLv2 and above" "$cve" "$cwe" "$hint"
+ elif [[ $sclient_success -eq 0 ]]; then
+ pr_svrty_low "VULNERABLE"; out ", uses 64 bit block ciphers"
+ fileout "SWEET32" "LOW" "uses 64 bit block ciphers" "$cve" "$cwe" "$hint"
+ elif "$ssl2_sweet"; then
+ pr_svrty_low "VULNERABLE"; out ", uses 64 bit block ciphers with SSLv2 only"
+ fileout "SWEET32" "LOW" "uses 64 bit block ciphers with SSLv2 only" "$cve" "$cwe" "$hint"
+ else
+ pr_svrty_best "not vulnerable (OK)";
+ if "$using_sockets"; then
+ fileout "SWEET32" "OK" "not vulnerable" "$cve" "$cwe"
+ else
+ if [[ "$nr_supported_ciphers" -ge 38 ]]; then
+ # Likely only PSK/KRB5 ciphers are missing: display discrepancy but no warning
+ if "$HAS_SSL2"; then
+ out ", $nr_supported_ciphers/$nr_sweet32_ciphers (SSLv2: $nr_ssl2_sweet32_ciphers/$nr_ssl2_supported_ciphers) local ciphers"
+ fileout "SWEET32" "OK" "not vulnerable ($nr_supported_ciphers of $nr_sweet32_ciphers (SSLv2: $nr_ssl2_sweet32_ciphers/$nr_ssl2_supported_ciphers)) local ciphers" "$cve" "$cwe"
+ else
+ out ", $nr_supported_ciphers/$nr_sweet32_ciphers local ciphers"
+ fileout "SWEET32" "OK" "not vulnerable ($nr_supported_ciphers of $nr_sweet32_ciphers local ciphers" "$cve" "$cwe"
+ fi
+ else
+ if "$HAS_SSL2"; then
+ pr_warning ", $nr_supported_ciphers/$nr_sweet32_ciphers (SSLv2: $nr_ssl2_sweet32_ciphers/$nr_ssl2_supported_ciphers) local ciphers"
+ fileout "SWEET32" "WARN" "not vulnerable but ($nr_supported_ciphers of $nr_sweet32_ciphers (SSLv2: $nr_ssl2_sweet32_ciphers/$nr_ssl2_supported_ciphers)) local ciphers only" "$cve" "$cwe"
+ else
+ pr_warning ", $nr_supported_ciphers/$nr_sweet32_ciphers local ciphers"
+ fileout "SWEET32" "WARN" "not vulnerable but ($nr_supported_ciphers of $nr_sweet32_ciphers) local ciphers only" "$cve" "$cwe"
+ fi
+ fi
+ fi
+ fi
+ outln
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ [[ $sclient_success -ge 6 ]] && return 1
+ return 0
+}
+
+
+# Padding Oracle On Downgraded Legacy Encryption, in a nutshell: don't use CBC Ciphers in SSLv3
+run_ssl_poodle() {
+ local -i sclient_success=0
+ local cbc_ciphers="ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:SRP-DSS-AES-256-CBC-SHA:SRP-RSA-AES-256-CBC-SHA:SRP-AES-256-CBC-SHA:DHE-PSK-AES256-CBC-SHA:DHE-RSA-AES256-SHA:DHE-DSS-AES256-SHA:DH-RSA-AES256-SHA:DH-DSS-AES256-SHA:DHE-RSA-CAMELLIA256-SHA:DHE-DSS-CAMELLIA256-SHA:DH-RSA-CAMELLIA256-SHA:DH-DSS-CAMELLIA256-SHA:AECDH-AES256-SHA:ADH-AES256-SHA:ADH-CAMELLIA256-SHA:ECDH-RSA-AES256-SHA:ECDH-ECDSA-AES256-SHA:AES256-SHA:ECDHE-PSK-AES256-CBC-SHA:CAMELLIA256-SHA:RSA-PSK-AES256-CBC-SHA:PSK-AES256-CBC-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:SRP-DSS-AES-128-CBC-SHA:SRP-RSA-AES-128-CBC-SHA:SRP-AES-128-CBC-SHA:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA:DH-RSA-AES128-SHA:DH-DSS-AES128-SHA:DHE-RSA-SEED-SHA:DHE-DSS-SEED-SHA:DH-RSA-SEED-SHA:DH-DSS-SEED-SHA:DHE-RSA-CAMELLIA128-SHA:DHE-DSS-CAMELLIA128-SHA:DH-RSA-CAMELLIA128-SHA:DH-DSS-CAMELLIA128-SHA:AECDH-AES128-SHA:ADH-AES128-SHA:ADH-SEED-SHA:ADH-CAMELLIA128-SHA:ECDH-RSA-AES128-SHA:ECDH-ECDSA-AES128-SHA:AES128-SHA:ECDHE-PSK-AES128-CBC-SHA:DHE-PSK-AES128-CBC-SHA:SEED-SHA:CAMELLIA128-SHA:IDEA-CBC-SHA:RSA-PSK-AES128-CBC-SHA:PSK-AES128-CBC-SHA:KRB5-IDEA-CBC-SHA:KRB5-IDEA-CBC-MD5:ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-SHA:SRP-DSS-3DES-EDE-CBC-SHA:SRP-RSA-3DES-EDE-CBC-SHA:SRP-3DES-EDE-CBC-SHA:EDH-RSA-DES-CBC3-SHA:EDH-DSS-DES-CBC3-SHA:DH-RSA-DES-CBC3-SHA:DH-DSS-DES-CBC3-SHA:AECDH-DES-CBC3-SHA:ADH-DES-CBC3-SHA:ECDH-RSA-DES-CBC3-SHA:ECDH-ECDSA-DES-CBC3-SHA:DES-CBC3-SHA:RSA-PSK-3DES-EDE-CBC-SHA:PSK-3DES-EDE-CBC-SHA:KRB5-DES-CBC3-SHA:KRB5-DES-CBC3-MD5:ECDHE-PSK-3DES-EDE-CBC-SHA:DHE-PSK-3DES-EDE-CBC-SHA:EXP1024-DHE-DSS-DES-CBC-SHA:EDH-RSA-DES-CBC-SHA:EDH-DSS-DES-CBC-SHA:DH-RSA-DES-CBC-SHA:DH-DSS-DES-CBC-SHA:ADH-DES-CBC-SHA:EXP1024-DES-CBC-SHA:DES-CBC-SHA:KRB5-DES-CBC-SHA:KRB5-DES-CBC-MD5:EXP-EDH-RSA-DES-CBC-SHA:EXP-EDH-DSS-DES-CBC-SHA:EXP-ADH-DES-CBC-SHA:EXP-DES-CBC-SHA:EXP-RC2-CBC-MD5:EXP-KRB5-RC2-CBC-SHA:EXP-KRB5-DES-CBC-SHA:EXP-KRB5-RC2-CBC-MD5:EXP-KRB5-DES-CBC-MD5:EXP-DH-DSS-DES-CBC-SHA:EXP-DH-RSA-DES-CBC-SHA"
+ local cbc_ciphers_hex="c0,14, c0,0a, c0,22, c0,21, c0,20, 00,91, 00,39, 00,38, 00,37, 00,36, 00,88, 00,87, 00,86, 00,85, c0,19, 00,3a, 00,89, c0,0f, c0,05, 00,35, c0,36, 00,84, 00,95, 00,8d, c0,13, c0,09, c0,1f, c0,1e, c0,1d, 00,33, 00,32, 00,31, 00,30, 00,9a, 00,99, 00,98, 00,97, 00,45, 00,44, 00,43, 00,42, c0,18, 00,34, 00,9b, 00,46, c0,0e, c0,04, 00,2f, c0,35, 00,90, 00,96, 00,41, 00,07, 00,94, 00,8c, 00,21, 00,25, c0,12, c0,08, c0,1c, c0,1b, c0,1a, 00,16, 00,13, 00,10, 00,0d, c0,17, 00,1b, c0,0d, c0,03, 00,0a, 00,93, 00,8b, 00,1f, 00,23, c0,34, 00,8f, 00,63, 00,15, 00,12, 00,0f, 00,0c, 00,1a, 00,62, 00,09, 00,1e, 00,22, 00,14, 00,11, 00,19, 00,08, 00,06, 00,27, 00,26, 00,2a, 00,29, 00,0b, 00,0e"
+ local hint=""
+ local -i nr_cbc_ciphers=0
+ local using_sockets=true
+ local cve="CVE-2014-3566"
+ local cwe="CWE-310"
+ local jsonID="POODLE_SSL"
+
+ [[ $VULN_COUNT -le $VULN_THRESHLD ]] && outln && pr_headlineln " Testing for SSLv3 POODLE (Padding Oracle On Downgraded Legacy Encryption) " && outln
+ pr_bold " POODLE, SSL"; out " ($cve) "
+
+ if "$TLS13_ONLY" || [[ $(has_server_protocol ssl3) -eq 1 ]]; then
+ # one condition should normally suffice but we don't know when run_poddle() was called
+ pr_svrty_best "not vulnerable (OK)"
+ outln ", no SSLv3 support"
+ fileout "$jsonID" "OK" "not vulnerable, no SSLv3" "$cve" "$cwe"
+ return 0
+ fi
+
+ "$SSL_NATIVE" && using_sockets=false
+ # The openssl binary distributed has almost everything we need (PSK and KRB5 ciphers are typically missing).
+ # Measurements show that there's little impact whether we use sockets or TLS here, so the default is sockets here
+ if "$using_sockets"; then
+ tls_sockets "00" "$cbc_ciphers_hex, 00,ff"
+ sclient_success=$?
+ else
+ if ! "$HAS_SSL3"; then
+ prln_local_problem "Your $OPENSSL doesn't support SSLv3"
+ return 1
+ fi
+ nr_cbc_ciphers=$(count_ciphers $cbc_ciphers)
+ nr_supported_ciphers=$(count_ciphers $(actually_supported_osslciphers $cbc_ciphers))
+ # SNI not needed as SSLv3 has none:
+ $OPENSSL s_client -ssl3 $STARTTLS $BUGS -cipher $cbc_ciphers -connect $NODEIP:$PORT $PROXY >$TMPFILE 2>$ERRFILE </dev/null
+ sclient_connect_successful $? $TMPFILE
+ sclient_success=$?
+ [[ "$DEBUG" -eq 2 ]] && grep -Eq "error|failure" $ERRFILE | grep -Eav "unable to get local|verify error"
+ fi
+ if [[ $sclient_success -eq 0 ]]; then
+ POODLE=0
+ pr_svrty_high "VULNERABLE (NOT ok)"; out ", uses SSLv3+CBC (check TLS_FALLBACK_SCSV mitigation below)"
+ fileout "$jsonID" "HIGH" "VULNERABLE, uses SSLv3+CBC" "$cve" "$cwe" "$hint"
+ else
+ POODLE=1
+ pr_svrty_best "not vulnerable (OK)";
+ if "$using_sockets"; then
+ fileout "$jsonID" "OK" "not vulnerable" "$cve" "$cwe"
+ else
+ if [[ "$nr_supported_ciphers" -ge 83 ]]; then
+ # Likely only KRB and PSK cipher are missing: display discrepancy but no warning
+ out ", $nr_supported_ciphers/$nr_cbc_ciphers local ciphers"
+ else
+ pr_warning ", $nr_supported_ciphers/$nr_cbc_ciphers local ciphers"
+ fi
+ fileout "$jsonID" "OK" "not vulnerable ($nr_supported_ciphers of $nr_cbc_ciphers local ciphers" "$cve" "$cwe"
+ fi
+ fi
+ outln
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 0
+}
+
+# for appliance which use padding, no fallback needed
+run_tls_poodle() {
+ local cve="CVE-2014-8730"
+ local cwe="CWE-310"
+ local jsonID="POODLE_TLS"
+
+ pr_bold " POODLE, TLS"; out " ($cve), experimental "
+ #FIXME
+ prln_warning "#FIXME"
+ fileout "$jsonID" "WARN" "Not yet implemented #FIXME" "$cve" "$cwe"
+ return 0
+}
+
+#FIXME: fileout needs to be patched according to new scheme. Postponed as otherwise merge fails ??
+#
+# This isn't a vulnerability check per se, but checks for the existence of
+# the countermeasure to protect against protocol downgrade attacks.
+#
+run_tls_fallback_scsv() {
+ local -i ret=0
+ local high_proto="" low_proto=""
+ local p high_proto_str protos_to_try
+ local jsonID="fallback_SCSV"
+
+ [[ $VULN_COUNT -le $VULN_THRESHLD ]] && outln && pr_headlineln " Testing for TLS_FALLBACK_SCSV Protection " && outln
+ pr_bold " TLS_FALLBACK_SCSV"; out " (RFC 7507) "
+
+ # First check we have support for TLS_FALLBACK_SCSV in our local OpenSSL
+ if ! "$HAS_FALLBACK_SCSV"; then
+ prln_local_problem "$OPENSSL lacks TLS_FALLBACK_SCSV support"
+ fileout "$jsonID" "WARN" "$OPENSSL lacks TLS_FALLBACK_SCSV support"
+ return 1
+ fi
+
+ # First determine the highest protocol that the server supports (not including TLSv1.3).
+ if [[ "$OPTIMAL_PROTO" == -ssl2 ]]; then
+ prln_svrty_critical "No fallback possible, SSLv2 is the only protocol"
+ fileout "$jsonID" "CRITICAL" "SSLv2 is the only protocol"
+ return 0
+ fi
+ for p in tls1_2 tls1_1 tls1 ssl3; do
+ [[ $(has_server_protocol "$p") -eq 1 ]] && continue
+ if [[ $(has_server_protocol "$p") -eq 0 ]]; then
+ high_proto="$p"
+ break
+ fi
+ [[ "$p" == ssl3 ]] && ! "$HAS_SSL3" && continue
+ $OPENSSL s_client $(s_client_options "-$p $STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY $SNI") >$TMPFILE 2>$ERRFILE </dev/null
+ if sclient_connect_successful $? $TMPFILE; then
+ high_proto="$p"
+ break
+ fi
+ done
+ case "$high_proto" in
+ "tls1_2")
+ high_proto_str="TLS 1.2"
+ protos_to_try="tls1_1 tls1 ssl3" ;;
+ "tls1_1")
+ high_proto_str="TLS 1.1"
+ protos_to_try="tls1 ssl3" ;;
+ "tls1")
+ high_proto_str="TLS 1"
+ protos_to_try="ssl3" ;;
+ "ssl3")
+ prln_svrty_high "No fallback possible, SSLv3 is the only protocol"
+ fileout "$jsonID" "HIGH" "only SSLv3 supported"
+ return 0
+ ;;
+ *) if [[ $(has_server_protocol tls1_3) -eq 0 ]]; then
+ # If the server supports TLS 1.3, and does not support TLS 1.2, TLS 1.1, or TLS 1,
+ # then assume it does not support SSLv3, even if SSLv3 cannot be tested.
+ pr_svrty_good "No fallback possible (OK)"; outln ", TLS 1.3 is the only protocol"
+ fileout "$jsonID" "OK" "only TLS 1.3 supported"
+ elif [[ $(has_server_protocol tls1_3) -eq 1 ]] && \
+ ( [[ $(has_server_protocol ssl3) -eq 1 ]] || "$HAS_SSL3" ); then
+ # TLS 1.3, TLS 1.2, TLS 1.1, TLS 1, and SSLv3 are all not supported.
+ # This may be an SSLv2-only server, if $OPENSSL does not support SSLv2.
+ prln_warning "test failed (couldn't connect)"
+ fileout "$jsonID" "WARN" "Check failed. (couldn't connect)"
+ return 1
+ elif [[ $(has_server_protocol tls1_3) -eq 1 ]]; then
+ # If the server does not support TLS 1.3, TLS 1.2, TLS 1.1, or TLS 1, and
+ # support for SSLv3 cannot be tested, then treat it as HIGH severity, since
+ # it is very likely that SSLv3 is the only supported protocol.
+ pr_svrty_high "NOT ok, no fallback possible"; outln ", TLS 1.3, 1.2, 1.1 and 1.0 not supported"
+ fileout "$jsonID" "HIGH" "TLS 1.3, 1.2, 1.1, 1.0 not supported"
+ else
+ # TLS 1.2, TLS 1.1, and TLS 1 are not supported, but can't tell whether TLS 1.3 is supported.
+ # This could be a TLS 1.3 only server, an SSLv3 only server (if SSLv3 support cannot be tested),
+ # or a server that does not support SSLv3 or any TLS protocol. So, don't report a severity,
+ # since this could either be good or bad.
+ outln "No fallback possible, TLS 1.2, TLS 1.1, and TLS 1 not supported"
+ fileout "$jsonID" "INFO" "TLS 1.2, TLS 1.1, and TLS 1 not supported"
+ fi
+ return 0
+ esac
+
+ # Next find a second protocol that the server supports.
+ for p in $protos_to_try; do
+ [[ $(has_server_protocol "$p") -eq 1 ]] && continue
+ if [[ $(has_server_protocol "$p") -eq 0 ]]; then
+ low_proto="$p"
+ break
+ fi
+ [[ "$p" == ssl3 ]] && ! "$HAS_SSL3" && continue
+ $OPENSSL s_client $(s_client_options "-$p $STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY $SNI") >$TMPFILE 2>$ERRFILE </dev/null
+ if sclient_connect_successful $? $TMPFILE; then
+ low_proto="$p"
+ break
+ fi
+ done
+
+ if ! "$HAS_SSL3" && \
+ ( [[ "$low_proto" == ssl3 ]] || \
+ ( [[ "$high_proto" == tls1 ]] && [[ $(has_server_protocol ssl3) -eq 2 ]] ) ); then
+ # If the protocol that the server would fall back to is SSLv3, but $OPENSSL does
+ # not support SSLv3, then the test cannot be performed. So, if $OPENSSL does not
+ # support SSLv3 and it is known that SSLv3 is the fallback protocol ($low_proto), then
+ # the test cannot be performed. Similarly, if SSLv3 could be the fallback protocol, but
+ # support for SSLv3 is unknown, then the test cannot be performed.
+ # NOTE: This check assumes that any server that supports SSLv3 and either TLS 1.2 or
+ # TLS 1.1 would also support TLS 1. So, if $high_proto is not TLS 1, then it is assumed
+ # that either (1) $low_proto has already been set (to TLS1.1 or TLS 1) or (2) no protocol
+ # lower than $high_proto is offered.
+ prln_local_problem "Can't test: $OPENSSL does not support SSLv3"
+ fileout "$jsonID" "WARN" "Can't test: $OPENSSL does not support SSLv3"
+ return 1
+ fi
+ if [[ -z "$low_proto" ]]; then
+ case "$high_proto" in
+ "tls1_2")
+ pr_svrty_good "No fallback possible (OK)"; outln ", no protocol below $high_proto_str offered"
+ ;;
+ *) outln "No fallback possible, no protocol below $high_proto_str offered (OK)"
+ ;;
+ esac
+ fileout "$jsonID" "OK" "no protocol below $high_proto_str offered"
+ return 0
+ fi
+ case "$low_proto" in
+ "tls1_1")
+ p="-no_tls1_2" ;;
+ "tls1")
+ p="-no_tls1_2 -no_tls1_1" ;;
+ "ssl3")
+ p="-no_tls1_2 -no_tls1_1 -no_tls1" ;;
+ esac
+ "$HAS_TLS13" && p+=" -no_tls1_3"
+ debugme echo "Simulating fallback from $high_proto to $low_proto"
+
+ # ...and do the test (we need to parse the error here!)
+ $OPENSSL s_client $(s_client_options "$STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY $SNI $p -fallback_scsv") &>$TMPFILE </dev/null
+ if grep -q "CONNECTED(00" "$TMPFILE"; then
+ if grep -qa "BEGIN CERTIFICATE" "$TMPFILE"; then
+ if [[ -z "$POODLE" ]]; then
+ pr_warning "Rerun including POODLE SSL check. "
+ pr_svrty_medium "Downgrade attack prevention NOT supported"
+ fileout "$jsonID" "WARN" "NOT supported. Pls rerun wity POODLE SSL check"
+ ret=1
+ elif [[ "$POODLE" -eq 0 ]]; then
+ pr_svrty_high "Downgrade attack prevention NOT supported and vulnerable to POODLE SSL"
+ fileout "$jsonID" "HIGH" "NOT supported and vulnerable to POODLE SSL"
+ else
+ pr_svrty_medium "Downgrade attack prevention NOT supported"
+ fileout "$jsonID" "MEDIUM" "NOT supported"
+ fi
+ elif grep -qa "alert inappropriate fallback" "$TMPFILE"; then
+ pr_svrty_good "Downgrade attack prevention supported (OK)"
+ fileout "$jsonID" "OK" "supported"
+ elif grep -qa "alert handshake failure" "$TMPFILE"; then
+ pr_svrty_good "Probably OK. "
+ fileout "$jsonID" "OK" "Probably oK"
+ # see RFC 7507, https://github.com/drwetter/testssl.sh/issues/121
+ # other case reported by Nicolas was F5 and at costumer of mine: the same
+ pr_svrty_medium "But received non-RFC-compliant \"handshake failure\" instead of \"inappropriate fallback\""
+ fileout "$jsonID" "MEDIUM" "received non-RFC-compliant \"handshake failure\" instead of \"inappropriate fallback\""
+ elif grep -qa "ssl handshake failure" "$TMPFILE"; then
+ pr_svrty_medium "some unexpected \"handshake failure\" instead of \"inappropriate fallback\""
+ fileout "$jsonID" "MEDIUM" "some unexpected \"handshake failure\" instead of \"inappropriate fallback\" (likely: warning)"
+ else
+ pr_warning "Check failed, unexpected result "
+ out ", run $PROG_NAME -Z --debug=1 and look at $TEMPDIR/*tls_fallback_scsv.txt"
+ fileout "$jsonID" "WARN" "Check failed, unexpected result, run $PROG_NAME -Z --debug=1 and look at $TEMPDIR/*tls_fallback_scsv.txt"
+ ret=1
+ fi
+ else
+ pr_warning "test failed (couldn't connect)"
+ fileout "$jsonID" "WARN" "Check failed. (couldn't connect)"
+ ret=1
+ fi
+
+ outln
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return $ret
+}
+
+
+# Factoring RSA Export Keys: don't use EXPORT RSA ciphers, see https://freakattack.com/
+run_freak() {
+ local -i sclient_success=0
+ local -i i nr_supported_ciphers=0 len
+ # with correct build it should list these 9 ciphers (plus the two latter as SSLv2 ciphers):
+ local exportrsa_cipher_list="EXP1024-DES-CBC-SHA:EXP1024-RC2-CBC-MD5:EXP1024-RC4-SHA:EXP1024-RC4-MD5:EXP-EDH-RSA-DES-CBC-SHA:EXP-DH-RSA-DES-CBC-SHA:EXP-DES-CBC-SHA:EXP-RC2-CBC-MD5:EXP-RC4-MD5"
+ local exportrsa_tls_cipher_list_hex="00,62, 00,61, 00,64, 00,60, 00,14, 00,0E, 00,08, 00,06, 00,03"
+ local exportrsa_ssl2_cipher_list_hex="04,00,80, 02,00,80, 00,00,00"
+ local detected_ssl2_ciphers
+ local addtl_warning="" hexc
+ local using_sockets=true
+ local cve="CVE-2015-0204"
+ local cwe="CWE-310"
+ local hint=""
+ local jsonID="FREAK"
+
+ [[ $VULN_COUNT -le $VULN_THRESHLD ]] && outln && pr_headlineln " Testing for FREAK attack " && outln
+ pr_bold " FREAK"; out " ($cve) "
+
+ if "$TLS13_ONLY"; then
+ pr_svrty_best "not vulnerable (OK)"
+ [[ $DEBUG -ge 1 ]] && out ", TLS 1.3 only server"
+ outln
+ fileout "$jsonID" "OK" "not vulnerable" "$cve" "$cwe"
+ return 0
+ fi
+
+ "$SSL_NATIVE" && using_sockets=false
+ if "$using_sockets"; then
+ nr_supported_ciphers=$(count_words "$exportrsa_tls_cipher_list_hex")+$(count_words "$exportrsa_ssl2_cipher_list_hex")
+ else
+ nr_supported_ciphers=$(count_ciphers $(actually_supported_osslciphers $exportrsa_cipher_list))
+ fi
+
+ case $nr_supported_ciphers in
+ 0) prln_local_problem "$OPENSSL doesn't have any EXPORT RSA ciphers configured"
+ fileout "$jsonID" "WARN" "Not tested. $OPENSSL doesn't have any EXPORT RSA ciphers configured" "$cve" "$cwe"
+ return 0
+ ;;
+ 1|2|3)
+ addtl_warning=" ($magenta""tested only with $nr_supported_ciphers out of 9 ciphers only!$off)" ;;
+ 4|5|6|7)
+ addtl_warning=" (tested with $nr_supported_ciphers/9 ciphers)" ;;
+ 8|9|10|11)
+ addtl_warning="" ;;
+ esac
+ if "$using_sockets"; then
+ tls_sockets "03" "$exportrsa_tls_cipher_list_hex, 00,ff"
+ sclient_success=$?
+ [[ $sclient_success -eq 2 ]] && sclient_success=0
+
+ # TLS handshake failed with ciphers above. Now we check SSLv2 -- unless we know it's not available
+ if [[ $sclient_success -ne 0 ]] && [[ $(has_server_protocol ssl2) -ne 1 ]]; then
+ sslv2_sockets "$exportrsa_ssl2_cipher_list_hex" "true"
+ if [[ $? -eq 3 ]] && [[ "$V2_HELLO_CIPHERSPEC_LENGTH" -ne 0 ]]; then
+ exportrsa_ssl2_cipher_list_hex="$(strip_spaces "${exportrsa_ssl2_cipher_list_hex//,/}")"
+ len=${#exportrsa_ssl2_cipher_list_hex}
+ detected_ssl2_ciphers="$(grep "Supported cipher: " "$TEMPDIR/$NODEIP.parse_sslv2_serverhello.txt")"
+ for (( i=0; i<len; i=i+6 )); do
+ [[ "$detected_ssl2_ciphers" =~ x${exportrsa_ssl2_cipher_list_hex:i:6} ]] && sclient_success=0 && break
+ done
+ fi
+ fi
+ else
+ $OPENSSL s_client $(s_client_options "$STARTTLS $BUGS -cipher $exportrsa_cipher_list -connect $NODEIP:$PORT $PROXY $SNI -no_ssl2") >$TMPFILE 2>$ERRFILE </dev/null
+ sclient_connect_successful $? $TMPFILE
+ sclient_success=$?
+ debugme grep -Ea "error|failure" $ERRFILE | grep -Eav "unable to get local|verify error"
+ if [[ $sclient_success -ne 0 ]] && "$HAS_SSL2"; then
+ $OPENSSL s_client $STARTTLS $BUGS -cipher $exportrsa_cipher_list -connect $NODEIP:$PORT $PROXY -ssl2 >$TMPFILE 2>$ERRFILE </dev/null
+ sclient_connect_successful $? $TMPFILE
+ sclient_success=$?
+ fi
+ fi
+ if [[ $sclient_success -eq 0 ]]; then
+ pr_svrty_critical "VULNERABLE (NOT ok)"; out ", uses EXPORT RSA ciphers"
+ fileout "$jsonID" "CRITICAL" "VULNERABLE, uses EXPORT RSA ciphers" "$cve" "$cwe" "$hint"
+ else
+ pr_svrty_best "not vulnerable (OK)"; out "$addtl_warning"
+ fileout "$jsonID" "OK" "not vulnerable $addtl_warning" "$cve" "$cwe"
+ fi
+ outln
+
+ if [[ $DEBUG -ge 2 ]]; then
+ if "$using_sockets"; then
+ for hexc in $(sed 's/, / /g' <<< "$exportrsa_tls_cipher_list_hex, $exportrsa_ssl2_cipher_list_hex"); do
+ if [[ ${#hexc} -eq 5 ]]; then
+ hexc="0x${hexc:0:2},0x${hexc:3:2}"
+ else
+ hexc="0x${hexc:0:2},0x${hexc:3:2},0x${hexc:6:2}"
+ fi
+ for (( i=0; i < TLS_NR_CIPHERS; i++ )); do
+ [[ "$hexc" == "${TLS_CIPHER_HEXCODE[i]}" ]] && break
+ done
+ [[ $i -eq $TLS_NR_CIPHERS ]] && tm_out "$hexc " || tm_out "${TLS_CIPHER_OSSL_NAME[i]} "
+ done
+ tmln_out
+ else
+ actually_supported_osslciphers $exportrsa_cipher_list
+ fi
+ fi
+ debugme echo $nr_supported_ciphers
+
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 0
+}
+
+# ARGs see below
+# Sets the global DH_GROUP_OFFERED, start value: "", after this function:
+# DH_GROUP_OFFERED=""
+# DH_GROUP_OFFERED="<name of group>"
+# return: 1: common primes file problem, 2: no pkey support, 0: went w/o error
+get_common_prime() {
+ local jsonID2="$1"
+ local key_bitstring="$2"
+ local spaces="$3"
+ local pubkey dh_p=""
+ local -i subret=0
+ local common_primes_file="$TESTSSL_INSTALL_DIR/etc/common-primes.txt"
+ local -i lineno_matched=0
+
+ "$HAS_PKEY" || return 2
+ pubkey="$($OPENSSL pkey -pubin -text_pub -noout 2>>$ERRFILE <<< "$key_bitstring")"
+ if [[ "$pubkey" =~ GROUP: ]]; then
+ DH_GROUP_OFFERED="${pubkey#*GROUP: }"
+ case "$DH_GROUP_OFFERED" in
+ modp_1536) DH_GROUP_OFFERED="RFC3526/Oakley Group 5" ;;
+ modp_2048) DH_GROUP_OFFERED="RFC3526/Oakley Group 14" ;;
+ modp_3072) DH_GROUP_OFFERED="RFC3526/Oakley Group 15" ;;
+ modp_4096) DH_GROUP_OFFERED="RFC3526/Oakley Group 16" ;;
+ modp_6144) DH_GROUP_OFFERED="RFC3526/Oakley Group 17" ;;
+ modp_8192) DH_GROUP_OFFERED="RFC3526/Oakley Group 18" ;;
+ dh_1024_160) DH_GROUP_OFFERED="RFC5114/1024-bit DSA group with 160-bit prime order subgroup" ;;
+ dh_2048_224) DH_GROUP_OFFERED="RFC5114/2048-bit DSA group with 224-bit prime order subgroup" ;;
+ dh_2048_256) DH_GROUP_OFFERED="RFC5114/2048-bit DSA group with 256-bit prime order subgroup" ;;
+ esac
+ pubkey="$(awk -F'(' '/Public-Key/ { print $2 }' <<< "$pubkey")"
+ DH_GROUP_LEN_P="${pubkey%% bit*}"
+ return 0
+ fi
+ dh_p="$(awk '/prime:|P:/,/generator:|G:/' <<< "$pubkey" | grep -Ev "prime|P:|generator|G:")"
+ dh_p="$(strip_spaces "$(colon_to_spaces "$(newline_to_spaces "$dh_p")")")"
+ [[ "${dh_p:0:2}" == "00" ]] && dh_p="${dh_p:2}"
+ DH_GROUP_LEN_P="$((4*${#dh_p}))"
+ debugme tmln_out "len(dh_p): $DH_GROUP_LEN_P | dh_p: $dh_p"
+ [[ "$DEBUG" -gt 1 ]] && echo "$dh_p" > $TEMPDIR/dh_p.txt
+ if [[ ! -s "$common_primes_file" ]]; then
+ prln_local_problem "couldn't read common primes file $common_primes_file"
+ out "${spaces}"
+ fileout "$jsonID2" "WARN" "couldn't read common primes file $common_primes_file"
+ return 1
+ else
+ dh_p="$(toupper "$dh_p")"
+ # In the previous line of the match is basically the hint we want to echo
+ # the most elegant thing to get the previous line [ awk '/regex/ { print x }; { x=$0 }' ] doesn't work with gawk
+ lineno_matched=$(grep -n "$dh_p" "$common_primes_file" 2>/dev/null | awk -F':' '{ print $1 }')
+ if [[ "$lineno_matched" -ne 0 ]]; then
+ DH_GROUP_OFFERED="$(awk "NR == $lineno_matched-1" "$common_primes_file" | awk -F'"' '{ print $2 }')"
+ #subret=1 # vulnerable: common prime
+ else
+ DH_GROUP_OFFERED="Unknown DH group"
+ :
+ #subret=0 # not vulnerable: no known common prime
+ fi
+ return 0
+ fi
+}
+
+
+# helper function for run_logjam see below
+#
+out_common_prime() {
+ local jsonID2="$1"
+ local cve="$2"
+ local cwe="$3"
+
+ [[ "$DH_GROUP_OFFERED" == ffdhe* ]] && [[ ! "$DH_GROUP_OFFERED" =~ \ ]] && DH_GROUP_OFFERED="RFC7919/$DH_GROUP_OFFERED"
+ if [[ "$DH_GROUP_OFFERED" =~ ffdhe ]] && [[ "$DH_GROUP_OFFERED" =~ \ ]]; then
+ out "common primes detected: "; pr_italic "$DH_GROUP_OFFERED"
+ fileout "$jsonID2" "INFO" "$DH_GROUP_OFFERED" "$cve" "$cwe"
+ # Now (below) size matters -- i.e. the bit size. As this is about a known prime we label it more strict.
+ # This needs maybe needs another thought as it could appear inconsistent with run_pfs and elsewhere.
+ # for now we label the bit size similar in the screen, but distinguish the leading text for logjam before
+ elif [[ $DH_GROUP_LEN_P -le 800 ]]; then
+ pr_svrty_critical "VULNERABLE (NOT ok):"; out " common prime: "
+ fileout "$jsonID2" "CRITICAL" "$DH_GROUP_OFFERED" "$cve" "$cwe"
+ pr_dh "$DH_GROUP_OFFERED" $DH_GROUP_LEN_P
+ elif [[ $DH_GROUP_LEN_P -le 1024 ]]; then
+ # really? Here we assume that 1024-bit common prime for nation states are worth and possible to precompute (TBC)
+ # otherwise 1024 are just medium
+ pr_svrty_high "VULNERABLE (NOT ok):"; out " common prime: "
+ fileout "$jsonID2" "HIGH" "$DH_GROUP_OFFERED" "$cve" "$cwe"
+ pr_dh "$DH_GROUP_OFFERED" $DH_GROUP_LEN_P
+ elif [[ $DH_GROUP_LEN_P -le 1536 ]]; then
+ pr_svrty_low "common prime: "
+ fileout "$jsonID2" "LOW" "$DH_GROUP_OFFERED" "$cve" "$cwe"
+ pr_dh "$DH_GROUP_OFFERED" $DH_GROUP_LEN_P
+ else
+ out "common prime with $DH_GROUP_LEN_P bits detected: "
+ fileout "$jsonID2" "INFO" "$DH_GROUP_OFFERED" "$cve" "$cwe"
+ pr_dh "$DH_GROUP_OFFERED" $DH_GROUP_LEN_P
+ fi
+}
+
+
+# see https://weakdh.org/logjam.html
+run_logjam() {
+ local -i sclient_success=0
+ local exportdh_cipher_list="EXP1024-DHE-DSS-DES-CBC-SHA:EXP1024-DHE-DSS-RC4-SHA:EXP-EDH-RSA-DES-CBC-SHA:EXP-EDH-DSS-DES-CBC-SHA"
+ local exportdh_cipher_list_hex="00,63, 00,65, 00,14, 00,11"
+ local all_dh_ciphers="cc,15, 00,b3, 00,91, c0,97, 00,a3, 00,9f, cc,aa, c0,a3, c0,9f, 00,6b, 00,6a, 00,39, 00,38, 00,c4, 00,c3, 00,88, 00,87, 00,a7, 00,6d, 00,3a, 00,c5, 00,89, 00,ab, cc,ad, c0,a7, c0,43, c0,45, c0,47, c0,53, c0,57, c0,5b, c0,67, c0,6d, c0,7d, c0,81, c0,85, c0,91, 00,a2, 00,9e, c0,a2, c0,9e, 00,aa, c0,a6, 00,67, 00,40, 00,33, 00,32, 00,be, 00,bd, 00,9a, 00,99, 00,45, 00,44, 00,a6, 00,6c, 00,34, 00,bf, 00,9b, 00,46, 00,b2, 00,90, c0,96, c0,42, c0,44, c0,46, c0,52, c0,56, c0,5a, c0,66, c0,6c, c0,7c, c0,80, c0,84, c0,90, 00,66, 00,18, 00,8e, 00,16, 00,13, 00,1b, 00,8f, 00,63, 00,15, 00,12, 00,1a, 00,65, 00,14, 00,11, 00,19, 00,17, 00,b5, 00,b4, 00,2d" # 93 ciphers
+ local -i i nr_supported_ciphers=0 server_key_exchange_len=0 ephemeral_pub_len=0
+ local addtl_warning="" hexc
+ local -i ret=0 subret=0
+ local server_key_exchange key_bitstring=""
+ local spaces=" "
+ local vuln_exportdh_ciphers=false
+ local openssl_no_expdhciphers=false
+ local str=""
+ local using_sockets=true
+ local cve="CVE-2015-4000"
+ local cwe="CWE-310"
+ local hint=""
+ local jsonID="LOGJAM"
+ local jsonID2="${jsonID}-common_primes"
+
+ [[ $VULN_COUNT -le $VULN_THRESHLD ]] && outln && pr_headlineln " Testing for LOGJAM vulnerability " && outln
+ pr_bold " LOGJAM"; out " ($cve), experimental "
+
+ "$SSL_NATIVE" && using_sockets=false
+ # Also as the openssl binary distributed has everything we need measurements show that
+ # there's no impact whether we use sockets or TLS here, so the default is sockets here
+ if ! "$using_sockets"; then
+ nr_supported_ciphers=$(count_ciphers $(actually_supported_osslciphers $exportdh_cipher_list))
+ debugme echo $nr_supported_ciphers
+ case $nr_supported_ciphers in
+ 0) prln_local_problem "$OPENSSL doesn't have any DH EXPORT ciphers configured"
+ fileout "$jsonID" "WARN" "Not tested. $OPENSSL doesn't support any DH EXPORT ciphers" "$cve" "$cwe"
+ out "$spaces"
+ openssl_no_expdhciphers=true
+ ;;
+ 1|2|3) addtl_warning=" ($magenta""tested w/ $nr_supported_ciphers/4 ciphers only!$off)" ;;
+ 4) ;;
+ esac
+ fi
+
+ # test for DH export ciphers first
+ if "$using_sockets"; then
+ tls_sockets "03" "$exportdh_cipher_list_hex, 00,ff"
+ sclient_success=$?
+ [[ $sclient_success -eq 2 ]] && sclient_success=0
+ [[ $sclient_success -eq 0 ]] && vuln_exportdh_ciphers=true
+ elif [[ $nr_supported_ciphers -ne 0 ]]; then
+ $OPENSSL s_client $(s_client_options "$STARTTLS $BUGS -cipher $exportdh_cipher_list -connect $NODEIP:$PORT $PROXY $SNI") >$TMPFILE 2>$ERRFILE </dev/null
+ sclient_connect_successful $? $TMPFILE
+ [[ $? -eq 0 ]] && vuln_exportdh_ciphers=true
+ debugme grep -Ea "error|failure" $ERRFILE | grep -Eav "unable to get local|verify error"
+ fi
+
+ if [[ $DEBUG -ge 2 ]]; then
+ if "$using_sockets"; then
+ for hexc in $(sed 's/, / /g' <<< "$exportdh_cipher_list_hex"); do
+ hexc="0x${hexc:0:2},0x${hexc:3:2}"
+ for (( i=0; i < TLS_NR_CIPHERS; i++ )); do
+ [[ "$hexc" == "${TLS_CIPHER_HEXCODE[i]}" ]] && break
+ done
+ [[ $i -eq $TLS_NR_CIPHERS ]] && tm_out "$hexc " || tm_out "${TLS_CIPHER_OSSL_NAME[i]} "
+ done
+ tmln_out
+ else
+ echo $(actually_supported_osslciphers $exportdh_cipher_list)
+ fi
+ fi
+
+ # Try all ciphers that use an ephemeral DH key. If successful, check whether the key uses a weak prime.
+ if [[ -n "$DH_GROUP_OFFERED" ]]; then
+ if [[ "$DH_GROUP_OFFERED" =~ Unknown ]]; then
+ subret=0 # no common DH key detected
+ else
+ subret=1 # known prime/DH key
+ fi
+ elif "$using_sockets"; then
+ tls_sockets "03" "$all_dh_ciphers, 00,ff" "ephemeralkey"
+ sclient_success=$?
+ if [[ $sclient_success -eq 0 ]] || [[ $sclient_success -eq 2 ]]; then
+ cp "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt" $TMPFILE
+ key_bitstring="$(awk '/-----BEGIN PUBLIC KEY/,/-----END PUBLIC KEY/ { print $0 }' $TMPFILE)"
+ fi
+ else
+ # FIXME: determine # of ciphers supported, 48 only are the shipped binaries
+ $OPENSSL s_client $(s_client_options "$STARTTLS $BUGS -cipher kEDH -msg -connect $NODEIP:$PORT $PROXY $SNI") >$TMPFILE 2>$ERRFILE </dev/null
+ sclient_connect_successful $? $TMPFILE
+ if [[ $? -eq 0 ]] && grep -q ServerKeyExchange $TMPFILE; then
+ # Example: '<<< TLS 1.0 Handshake [length 010b], ServerKeyExchange'
+ # get line with ServerKeyExchange, cut from the beginning to "length ". cut from the end to ']'
+ str="$(awk '/<<< TLS 1.[0-2].*ServerKeyExchange$/' $TMPFILE)"
+ if [[ -z "$str" ]] ; then
+ str="$(awk '/<<< SSL [2-3].*ServerKeyExchange$/' $TMPFILE)"
+ fi
+ str="${str#<*length }"
+ str="${str%]*}"
+ server_key_exchange_len=$(hex2dec "$str")
+ server_key_exchange_len=2+$server_key_exchange_len/16
+ server_key_exchange="$(grep -A $server_key_exchange_len ServerKeyExchange $TMPFILE | tail -n +2)"
+ server_key_exchange="$(toupper "$(strip_spaces "$(newline_to_spaces "$server_key_exchange")")")"
+ server_key_exchange="${server_key_exchange%%[!0-9A-F]*}"
+ server_key_exchange_len=${#server_key_exchange}
+ [[ $server_key_exchange_len -gt 8 ]] && [[ "${server_key_exchange:0:2}" == "0C" ]] && ephemeral_pub_len=$(hex2dec "${server_key_exchange:2:6}")
+ [[ $ephemeral_pub_len -ne 0 ]] && [[ $ephemeral_pub_len -le $server_key_exchange_len ]] && key_bitstring="$(get_dh_ephemeralkey "${server_key_exchange:8}")"
+ fi
+ fi
+
+ if [[ -n "$key_bitstring" ]]; then
+ if [[ -z "$DH_GROUP_OFFERED" ]]; then
+ get_common_prime "$jsonID2" "$key_bitstring" "$spaces"
+ ret=$? # no common primes file would be ret=1 --> we should treat that some place else before
+ fi
+ if [[ "$DH_GROUP_OFFERED" =~ Unknown ]]; then
+ subret=0 # no common DH key detected
+ else
+ subret=1 # known prime/DH key
+ fi
+ elif [[ -z "$DH_GROUP_OFFERED" ]]; then
+ subret=3
+ fi
+
+ # Now if we have DH export ciphers we print them out first
+ if "$vuln_exportdh_ciphers"; then
+ pr_svrty_high "VULNERABLE (NOT ok):"; out " uses DH EXPORT ciphers"
+ fileout "$jsonID" "HIGH" "VULNERABLE, uses DH EXPORT ciphers" "$cve" "$cwe" "$hint"
+ if [[ $subret -eq 3 ]]; then
+ out ", no DH key detected with <= TLS 1.2"
+ fileout "$jsonID2" "OK" "no DH key detected with <= TLS 1.2"
+ elif [[ $subret -eq 1 ]]; then
+ out "\n${spaces}"
+ out_common_prime "$jsonID2" "$cve" "$cwe"
+ elif [[ $subret -eq 0 ]]; then
+ out " no common primes detected"
+ fileout "$jsonID2" "INFO" "--" "$cve" "$cwe"
+ elif [[ $ret -eq 1 ]]; then
+ out "FIXME 1"
+ fi
+ else
+ if [[ $subret -eq 1 ]]; then
+ out_common_prime "$jsonID2" "$cve" "$cwe"
+ if ! "$openssl_no_expdhciphers"; then
+ outln ","
+ out "${spaces}but no DH EXPORT ciphers${addtl_warning}"
+ fileout "$jsonID" "OK" "not vulnerable, no DH EXPORT ciphers,$addtl_warning" "$cve" "$cwe"
+ fi
+ elif [[ $subret -eq 3 ]]; then
+ pr_svrty_good "not vulnerable (OK):"; out " no DH EXPORT ciphers${addtl_warning}"
+ fileout "$jsonID" "OK" "not vulnerable, no DH EXPORT ciphers,$addtl_warning" "$cve" "$cwe"
+ out ", no DH key detected with <= TLS 1.2"
+ fileout "$jsonID2" "OK" "no DH key with <= TLS 1.2" "$cve" "$cwe"
+ elif [[ $subret -eq 0 ]]; then
+ pr_svrty_good "not vulnerable (OK):"; out " no DH EXPORT ciphers${addtl_warning}"
+ fileout "$jsonID" "OK" "not vulnerable, no DH EXPORT ciphers,$addtl_warning" "$cve" "$cwe"
+ # we issue a special warning if there's no common prime but the bit length is too low
+ if [[ $DH_GROUP_LEN_P -le 1024 ]]; then
+ out "\n${spaces}But: "
+ pr_dh "$DH_GROUP_OFFERED" $DH_GROUP_LEN_P
+ case $? in
+ 1) fileout "$jsonID" "CRITICAL" "no DH EXPORT ciphers, no common prime but $DH_GROUP_OFFERED has only $DH_GROUP_LEN_P bits, $addtl_warning" "$cve" "$cwe" ;;
+ 2) fileout "$jsonID" "HIGH" "no DH EXPORT ciphers, no common prime but $DH_GROUP_OFFERED has only $DH_GROUP_LEN_P bits, $addtl_warning" "$cve" "$cwe";;
+ 3) fileout "$jsonID" "MEDIUM" "no DH EXPORT ciphers, no common prime but $DH_GROUP_OFFERED has only $DH_GROUP_LEN_P bits, $addtl_warning" "$cve" "$cwe";;
+ esac
+ else
+ out ", no common prime detected"
+ fileout "$jsonID2" "OK" "--" "$cve" "$cwe"
+ fi
+ elif [[ $ret -eq 1 ]]; then
+ pr_svrty_good "partly not vulnerable:"; out " no DH EXPORT ciphers${addtl_warning}"
+ fileout "$jsonID" "OK" "not vulnerable, no DH EXPORT ciphers,$addtl_warning" "$cve" "$cwe"
+ fi
+ fi
+
+ outln
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return $ret
+}
+
+# Decrypting RSA with Obsolete and Weakened eNcryption, more @ https://drownattack.com/
+run_drown() {
+ local -i nr_ciphers_detected ret=0
+ local spaces=" "
+ local cert_fingerprint_sha2=""
+ local cve="CVE-2016-0800 CVE-2016-0703"
+ local cwe="CWE-310"
+ local hint=""
+ local jsonID="DROWN"
+
+ if [[ $VULN_COUNT -le $VULN_THRESHLD ]]; then
+ outln
+ pr_headlineln " Testing for DROWN vulnerability "
+ outln
+ fi
+# if we want to use OPENSSL: check for < openssl 1.0.2g, openssl 1.0.1s if native openssl
+ pr_bold " DROWN"; out " (${cve// /, }) "
+
+ # Any fingerprint that is placed in $RSA_CERT_FINGERPRINT_SHA2 is also added to
+ # to $CERT_FINGERPRINT_SHA2, so if $CERT_FINGERPRINT_SHA2 is not empty, but
+ # $RSA_CERT_FINGERPRINT_SHA2 is empty, then the server doesn't have an RSA certificate.
+ if [[ -z "$CERT_FINGERPRINT_SHA2" ]]; then
+ get_host_cert "-cipher aRSA"
+ [[ $? -eq 0 ]] && cert_fingerprint_sha2="$($OPENSSL x509 -noout -in $HOSTCERT -fingerprint -sha256 2>>$ERRFILE | sed -e 's/^.*Fingerprint=//' -e 's/://g' )"
+ else
+ cert_fingerprint_sha2="$RSA_CERT_FINGERPRINT_SHA2"
+ cert_fingerprint_sha2=${cert_fingerprint_sha2/SHA256 /}
+ fi
+
+ if ( [[ "$STARTTLS_PROTOCOL" =~ ldap ]] || [[ "$STARTTLS_PROTOCOL" =~ irc ]] ); then
+ prln_local_problem "STARTTLS/$STARTTLS_PROTOCOL and --ssl-native collide here"
+ return 1
+ fi
+
+ if [[ $(has_server_protocol ssl2) -ne 1 ]]; then
+ sslv2_sockets
+ else
+ [[ aaa == bbb ]] # provoke retrurn code=1
+ fi
+
+ case $? in
+ 7) # strange reply, couldn't convert the cipher spec length to a hex number
+ pr_fixme "strange v2 reply "
+ outln " (rerun with DEBUG >=2)"
+ [[ $DEBUG -ge 3 ]] && hexdump -C "$TEMPDIR/$NODEIP.sslv2_sockets.dd" | head -1
+ fileout "$jsonID" "WARN" "received a strange SSLv2 reply (rerun with DEBUG>=2)" "$cve" "$cwe"
+ ret=1
+ ;;
+ 3) # vulnerable, [[ -n "$cert_fingerprint_sha2" ]] test is not needed as we should have RSA certificate here
+ lines=$(count_lines "$(hexdump -C "$TEMPDIR/$NODEIP.sslv2_sockets.dd" 2>/dev/null)")
+ debugme tm_out " ($lines lines) "
+ add_tls_offered ssl2 yes
+ if [[ "$lines" -gt 1 ]]; then
+ nr_ciphers_detected=$((V2_HELLO_CIPHERSPEC_LENGTH / 3))
+ if [[ 0 -eq "$nr_ciphers_detected" ]]; then
+ prln_svrty_high "CVE-2015-3197: SSLv2 supported but couldn't detect a cipher (NOT ok)";
+ fileout "$jsonID" "HIGH" "SSLv2 offered, but could not detect a cipher. Make sure you don't use this certificate elsewhere, see https://censys.io/ipv4?q=$cert_fingerprint_sha2" "$cve CVE-2015-3197" "$cwe" "$hint"
+ else
+ prln_svrty_critical "VULNERABLE (NOT ok), SSLv2 offered with $nr_ciphers_detected ciphers";
+ fileout "$jsonID" "CRITICAL" "VULNERABLE, SSLv2 offered with $nr_ciphers_detected ciphers. Make sure you don't use this certificate elsewhere, see https://censys.io/ipv4?q=$cert_fingerprint_sha2" "$cve" "$cwe" "$hint"
+ fi
+ outln "$spaces Make sure you don't use this certificate elsewhere, see:"
+ out "$spaces "
+ pr_url "https://censys.io/ipv4?q=$cert_fingerprint_sha2"
+ outln
+ fi
+ ;;
+ *) prln_svrty_best "not vulnerable on this host and port (OK)"
+ fileout "$jsonID" "OK" "not vulnerable on this host and port" "$cve" "$cwe"
+ if [[ -n "$cert_fingerprint_sha2" ]]; then
+ outln "$spaces make sure you don't use this certificate elsewhere with SSLv2 enabled services"
+ out "$spaces "
+ pr_url "https://censys.io/ipv4?q=$cert_fingerprint_sha2"
+ outln " could help you to find out"
+ fileout "${jsonID}_hint" "INFO" "Make sure you don't use this certificate elsewhere with SSLv2 enabled services, see https://censys.io/ipv4?q=$cert_fingerprint_sha2" "$cve" "$cwe"
+ else
+ outln "$spaces no RSA certificate, thus certificate can't be used with SSLv2 elsewhere"
+ fileout "${jsonID}_hint" "INFO" "no RSA certificate, can't be used with SSLv2 elsewhere" "$cve" "$cwe"
+ fi
+ ;;
+ esac
+
+ return $ret
+}
+
+
+
+# Browser Exploit Against SSL/TLS: don't use CBC Ciphers in SSLv3 TLSv1.0
+run_beast(){
+ local hexc dash cbc_cipher sslvers auth mac export
+ local -a ciph hexcode normalized_hexcode kx enc export2
+ local proto proto_hex
+ local -i i subret nr_ciphers=0 sclient_success=0
+ local detected_cbc_ciphers="" ciphers_to_test
+ local higher_proto_supported=""
+ local vuln_beast=false
+ local spaces=" "
+ local cr=$'\n'
+ local first=true
+ local continued=false
+ local cbc_cipher_list="EXP-RC2-CBC-MD5:IDEA-CBC-SHA:EXP-DES-CBC-SHA:DES-CBC-SHA:DES-CBC3-SHA:EXP-DH-DSS-DES-CBC-SHA:DH-DSS-DES-CBC-SHA:DH-DSS-DES-CBC3-SHA:EXP-DH-RSA-DES-CBC-SHA:DH-RSA-DES-CBC-SHA:DH-RSA-DES-CBC3-SHA:EXP-EDH-DSS-DES-CBC-SHA:EDH-DSS-DES-CBC-SHA:EDH-DSS-DES-CBC3-SHA:EXP-EDH-RSA-DES-CBC-SHA:EDH-RSA-DES-CBC-SHA:EDH-RSA-DES-CBC3-SHA:EXP-ADH-DES-CBC-SHA:ADH-DES-CBC-SHA:ADH-DES-CBC3-SHA:KRB5-DES-CBC-SHA:KRB5-DES-CBC3-SHA:KRB5-IDEA-CBC-SHA:KRB5-DES-CBC-MD5:KRB5-DES-CBC3-MD5:KRB5-IDEA-CBC-MD5:EXP-KRB5-DES-CBC-SHA:EXP-KRB5-RC2-CBC-SHA:EXP-KRB5-DES-CBC-MD5:EXP-KRB5-RC2-CBC-MD5:AES128-SHA:DH-DSS-AES128-SHA:DH-RSA-AES128-SHA:DHE-DSS-AES128-SHA:DHE-RSA-AES128-SHA:ADH-AES128-SHA:AES256-SHA:DH-DSS-AES256-SHA:DH-RSA-AES256-SHA:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:ADH-AES256-SHA:CAMELLIA128-SHA:DH-DSS-CAMELLIA128-SHA:DH-RSA-CAMELLIA128-SHA:DHE-DSS-CAMELLIA128-SHA:DHE-RSA-CAMELLIA128-SHA:ADH-CAMELLIA128-SHA:EXP1024-RC2-CBC-MD5:EXP1024-DES-CBC-SHA:EXP1024-DHE-DSS-DES-CBC-SHA:CAMELLIA256-SHA:DH-DSS-CAMELLIA256-SHA:DH-RSA-CAMELLIA256-SHA:DHE-DSS-CAMELLIA256-SHA:DHE-RSA-CAMELLIA256-SHA:ADH-CAMELLIA256-SHA:PSK-3DES-EDE-CBC-SHA:PSK-AES128-CBC-SHA:PSK-AES256-CBC-SHA:DHE-PSK-3DES-EDE-CBC-SHA:DHE-PSK-AES128-CBC-SHA:DHE-PSK-AES256-CBC-SHA:RSA-PSK-3DES-EDE-CBC-SHA:RSA-PSK-AES128-CBC-SHA:RSA-PSK-AES256-CBC-SHA:SEED-SHA:DH-DSS-SEED-SHA:DH-RSA-SEED-SHA:DHE-DSS-SEED-SHA:DHE-RSA-SEED-SHA:ADH-SEED-SHA:PSK-AES128-CBC-SHA256:PSK-AES256-CBC-SHA384:DHE-PSK-AES128-CBC-SHA256:DHE-PSK-AES256-CBC-SHA384:RSA-PSK-AES128-CBC-SHA256:RSA-PSK-AES256-CBC-SHA384:ECDH-ECDSA-DES-CBC3-SHA:ECDH-ECDSA-AES128-SHA:ECDH-ECDSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA:ECDH-RSA-DES-CBC3-SHA:ECDH-RSA-AES128-SHA:ECDH-RSA-AES256-SHA:ECDHE-RSA-DES-CBC3-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:AECDH-DES-CBC3-SHA:AECDH-AES128-SHA:AECDH-AES256-SHA:SRP-3DES-EDE-CBC-SHA:SRP-RSA-3DES-EDE-CBC-SHA:SRP-DSS-3DES-EDE-CBC-SHA:SRP-AES-128-CBC-SHA:SRP-RSA-AES-128-CBC-SHA:SRP-DSS-AES-128-CBC-SHA:SRP-AES-256-CBC-SHA:SRP-RSA-AES-256-CBC-SHA:SRP-DSS-AES-256-CBC-SHA:ECDHE-PSK-3DES-EDE-CBC-SHA:ECDHE-PSK-AES128-CBC-SHA:ECDHE-PSK-AES256-CBC-SHA:ECDHE-PSK-AES128-CBC-SHA256:ECDHE-PSK-AES256-CBC-SHA384:PSK-CAMELLIA128-SHA256:PSK-CAMELLIA256-SHA384:DHE-PSK-CAMELLIA128-SHA256:DHE-PSK-CAMELLIA256-SHA384:RSA-PSK-CAMELLIA128-SHA256:RSA-PSK-CAMELLIA256-SHA384:ECDHE-PSK-CAMELLIA128-SHA256:ECDHE-PSK-CAMELLIA256-SHA384"
+ local cbc_ciphers_hex="00,06, 00,07, 00,08, 00,09, 00,0A, 00,0B, 00,0C, 00,0D, 00,0E, 00,0F, 00,10, 00,11, 00,12, 00,13, 00,14, 00,15, 00,16, 00,19, 00,1A, 00,1B, 00,1E, 00,1F, 00,21, 00,22, 00,23, 00,25, 00,26, 00,27, 00,29, 00,2A, 00,2F, 00,30, 00,31, 00,32, 00,33, 00,34, 00,35, 00,36, 00,37, 00,38, 00,39, 00,3A, 00,41, 00,42, 00,43, 00,44, 00,45, 00,46, 00,61, 00,62, 00,63, 00,84, 00,85, 00,86, 00,87, 00,88, 00,89, 00,8B, 00,8C, 00,8D, 00,8F, 00,90, 00,91, 00,93, 00,94, 00,95, 00,96, 00,97, 00,98, 00,99, 00,9A, 00,9B, 00,AE, 00,AF, 00,B2, 00,B3, 00,B6, 00,B7, C0,03, C0,04, C0,05, C0,08, C0,09, C0,0A, C0,0D, C0,0E, C0,0F, C0,12, C0,13, C0,14, C0,17, C0,18, C0,19, C0,1A, C0,1B, C0,1C, C0,1D, C0,1E, C0,1F, C0,21, C0,22, C0,34, C0,35, C0,36, C0,37, C0,38, C0,64, C0,65, C0,66, C0,67, C0,68, C0,69, C0,70, C0,71, C0,94, C0,95, C0,96, C0,97, C0,98, C0,99, C0,9A, C0,9B, FE,FE, FE,FF, FF,E0, FF,E1"
+ local has_dh_bits="$HAS_DH_BITS"
+ local using_sockets=true
+ local cve="CVE-2011-3389"
+ local cwe="CWE-20"
+ local hint=""
+ local jsonID="BEAST"
+
+ if [[ $VULN_COUNT -le $VULN_THRESHLD ]]; then
+ outln
+ pr_headlineln " Testing for BEAST vulnerability "
+ outln
+ fi
+ pr_bold " BEAST"; out " ($cve) "
+
+ if "$TLS13_ONLY" || ( [[ $(has_server_protocol ssl3) -eq 1 ]] && [[ $(has_server_protocol tls1) -eq 1 ]] ); then
+ pr_svrty_good "not vulnerable (OK)"
+ outln ", no SSL3 or TLS1"
+ fileout "$jsonID" "OK" "not vulnerable, no SSL3 or TLS1" "$cve" "$cwe"
+ return 0
+ fi
+
+ "$SSL_NATIVE" && using_sockets=false
+ # $cbc_ciphers_hex has 126 ciphers, we omitted SRP-AES-256-CBC-SHA bc the trailing 00,ff below will pose
+ # a problem for ACE loadbalancers otherwise. So in case we know this is not true, we'll re-add it
+ ! "$SERVER_SIZE_LIMIT_BUG" && "$using_sockets" && cbc_ciphers_hex="$cbc_ciphers_hex, C0,20"
+
+ [[ $TLS_NR_CIPHERS == 0 ]] && using_sockets=false
+ if "$using_sockets" || [[ $OSSL_VER_MAJOR -lt 1 ]]; then
+ for (( i=0; i < TLS_NR_CIPHERS; i++ )); do
+ hexc="${TLS_CIPHER_HEXCODE[i]}"
+ if [[ ${#hexc} -eq 9 ]] && [[ "${TLS_CIPHER_RFC_NAME[i]}" =~ CBC ]] && \
+ [[ ! "${TLS_CIPHER_RFC_NAME[i]}" =~ SHA256 ]] && [[ ! "${TLS_CIPHER_RFC_NAME[i]}" =~ SHA384 ]]; then
+ ciph[nr_ciphers]="${TLS_CIPHER_OSSL_NAME[i]}"
+ hexcode[nr_ciphers]="${hexc:2:2},${hexc:7:2}"
+ rfc_ciph[nr_ciphers]="${TLS_CIPHER_RFC_NAME[i]}"
+ kx[nr_ciphers]="${TLS_CIPHER_KX[i]}"
+ enc[nr_ciphers]="${TLS_CIPHER_ENC[i]}"
+ export2[nr_ciphers]="${TLS_CIPHER_EXPORT[i]}"
+ ossl_supported[nr_ciphers]=${TLS_CIPHER_OSSL_SUPPORTED[i]}
+ if "$using_sockets" && "$WIDE" && ! "$has_dh_bits" && \
+ ( [[ ${kx[nr_ciphers]} == Kx=ECDH ]] || [[ ${kx[nr_ciphers]} == Kx=DH ]] || [[ ${kx[nr_ciphers]} == Kx=EDH ]] ); then
+ ossl_supported[nr_ciphers]=false
+ fi
+ if [[ "${hexc:2:2}" == 00 ]]; then
+ normalized_hexcode[nr_ciphers]="x${hexc:7:2}"
+ else
+ normalized_hexcode[nr_ciphers]="x${hexc:2:2}${hexc:7:2}"
+ fi
+ nr_ciphers+=1
+ fi
+ done
+ else
+ # no sockets, openssl
+ while read hexc dash ciph[nr_ciphers] sslvers kx[nr_ciphers] auth enc[nr_ciphers] mac export2[nr_ciphers]; do
+ if [[ ":${cbc_cipher_list}:" =~ :${ciph[nr_ciphers]}: ]]; then
+ ossl_supported[nr_ciphers]=true
+ if [[ "${hexc:2:2}" == "00" ]]; then
+ normalized_hexcode[nr_ciphers]="x${hexc:7:2}"
+ else
+ normalized_hexcode[nr_ciphers]="x${hexc:2:2}${hexc:7:2}"
+ fi
+ nr_ciphers+=1
+ fi
+ done < <(actually_supported_osslciphers 'ALL:COMPLEMENTOFALL:@STRENGTH' 'ALL' "-tls1 -V")
+ fi
+
+ # first determine whether it's mitigated by higher protocols
+ for proto in tls1_1 tls1_2; do
+ subret=$(has_server_protocol "$proto")
+ if [[ $subret -eq 0 ]]; then
+ case $proto in
+ tls1_1) higher_proto_supported+=" TLSv1.1" ;;
+ tls1_2) higher_proto_supported+=" TLSv1.2" ;;
+ esac
+ elif [[ $subret -eq 2 ]]; then
+ $OPENSSL s_client $(s_client_options "-state -"${proto}" $STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY $SNI") 2>>$ERRFILE >$TMPFILE </dev/null
+ if sclient_connect_successful $? $TMPFILE; then
+ higher_proto_supported+=" $(get_protocol $TMPFILE)"
+ add_tls_offered "$proto" yes
+ fi
+ fi
+ done
+
+ for proto in ssl3 tls1; do
+ if [[ "$proto" == ssl3 ]] && ! "$using_sockets" && ! locally_supported "-${proto}"; then
+ continued=true
+ out " "
+ continue
+ fi
+ subret=$(has_server_protocol "$proto")
+ if [[ $subret -eq 0 ]]; then
+ sclient_success=0
+ elif [[ $subret -eq 1 ]]; then
+ sclient_success=1
+ elif [[ "$proto" != "ssl3" ]] || "$HAS_SSL3"; then
+ $OPENSSL s_client $(s_client_options "-"$proto" $STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY $SNI") >$TMPFILE 2>>$ERRFILE </dev/null
+ sclient_connect_successful $? $TMPFILE
+ sclient_success=$?
+ else
+ tls_sockets "00" "$TLS_CIPHER"
+ sclient_success=$?
+ fi
+ if [[ $sclient_success -ne 0 ]]; then # protocol supported?
+ if "$continued"; then # second round: we hit TLS1
+ if "$HAS_SSL3" || "$using_sockets"; then
+ pr_svrty_good "not vulnerable (OK)" ; outln ", no SSL3 or TLS1"
+ fileout "$jsonID" "OK" "not vulnerable, no SSL3 or TLS1" "$cve" "$cwe"
+ else
+ prln_svrty_good "no TLS1 (OK)"
+ fileout "$jsonID" "OK" "not vulnerable, no TLS1" "$cve" "$cwe"
+ fi
+ return 0
+ else # protocol not succeeded but it's the first time
+ continued=true
+ continue # protocol not supported, so we do not need to check each cipher with that protocol
+ fi
+ fi # protocol succeeded
+ add_tls_offered "$proto" yes
+
+ # now we test in one shot with the precompiled ciphers
+ if "$using_sockets"; then
+ case "$proto" in
+ "ssl3") proto_hex="00" ;;
+ "tls1") proto_hex="01" ;;
+ esac
+ tls_sockets "$proto_hex" "$cbc_ciphers_hex, 00,ff"
+ [[ $? -eq 0 ]] || continue
+ else
+ $OPENSSL s_client $(s_client_options "-"$proto" -cipher "$cbc_cipher_list" $STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY $SNI") >$TMPFILE 2>>$ERRFILE </dev/null
+ sclient_connect_successful $? $TMPFILE || continue
+ fi
+
+ detected_cbc_ciphers=""
+ for ((i=0; i<nr_ciphers; i++)); do
+ ciphers_found[i]=false
+ sigalg[nr_ciphers]=""
+ done
+ while true; do
+ [[ "$proto" == ssl3 ]] && ! "$HAS_SSL3" && break
+ ciphers_to_test=""
+ for (( i=0; i < nr_ciphers; i++ )); do
+ ! "${ciphers_found[i]}" && "${ossl_supported[i]}" && ciphers_to_test+=":${ciph[i]}"
+ done
+ [[ -z "$ciphers_to_test" ]] && break
+ $OPENSSL s_client $(s_client_options "-cipher "${ciphers_to_test:1}" -"${proto}" $STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY $SNI") >$TMPFILE 2>>$ERRFILE </dev/null
+ sclient_connect_successful $? $TMPFILE || break
+ cbc_cipher=$(get_cipher $TMPFILE)
+ [[ -z "$cbc_cipher" ]] && break
+ for (( i=0; i < nr_ciphers; i++ )); do
+ [[ "$cbc_cipher" == "${ciph[i]}" ]] && break
+ done
+ ciphers_found[i]=true
+ if [[ "$DISPLAY_CIPHERNAMES" =~ openssl ]] || [[ "${rfc_ciph[i]}" == - ]]; then
+ detected_cbc_ciphers+="${ciph[i]} "
+ else
+ detected_cbc_ciphers+="${rfc_ciph[i]} "
+ fi
+ vuln_beast=true
+ if "$WIDE" && ( [[ ${kx[i]} == Kx=ECDH ]] || [[ ${kx[i]} == Kx=DH ]] || [[ ${kx[i]} == Kx=EDH ]] ); then
+ dhlen=$(read_dhbits_from_file "$TMPFILE" quiet)
+ kx[i]="${kx[i]} $dhlen"
+ fi
+ "$WIDE" && "$SHOW_SIGALGO" && grep -q "\-\-\-\-\-BEGIN CERTIFICATE\-\-\-\-\-" $TMPFILE && \
+ sigalg[i]="$(read_sigalg_from_file "$TMPFILE")"
+ done
+ if "$using_sockets"; then
+ while true; do
+ ciphers_to_test=""
+ for (( i=0; i < nr_ciphers; i++ )); do
+ ! "${ciphers_found[i]}" && ciphers_to_test+=", ${hexcode[i]}"
+ done
+ [[ -z "$ciphers_to_test" ]] && break
+ if "$SHOW_SIGALGO"; then
+ tls_sockets "$proto_hex" "${ciphers_to_test:2}, 00,ff" "all"
+ else
+ tls_sockets "$proto_hex" "${ciphers_to_test:2}, 00,ff" "ephemeralkey"
+ fi
+ [[ $? -ne 0 ]] && break
+ cbc_cipher=$(get_cipher "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt")
+ for (( i=0; i < nr_ciphers; i++ )); do
+ [[ "$cbc_cipher" == "${rfc_ciph[i]}" ]] && break
+ done
+ [[ $i -eq $nr_ciphers ]] && break
+ ciphers_found[i]=true
+ if ( [[ "$DISPLAY_CIPHERNAMES" =~ openssl ]] && [[ "${ciph[i]}" != - ]] ) || [[ "${rfc_ciph[i]}" == - ]]; then
+ detected_cbc_ciphers+="${ciph[i]} "
+ else
+ detected_cbc_ciphers+="${rfc_ciph[i]} "
+ fi
+ vuln_beast=true
+ if "$WIDE" && ( [[ ${kx[i]} == Kx=ECDH ]] || [[ ${kx[i]} == Kx=DH ]] || [[ ${kx[i]} == Kx=EDH ]] ); then
+ dhlen=$(read_dhbits_from_file "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt" quiet)
+ kx[i]="${kx[i]} $dhlen"
+ fi
+ "$WIDE" && "$SHOW_SIGALGO" && [[ -r "$HOSTCERT" ]] && \
+ sigalg[i]="$(read_sigalg_from_file "$HOSTCERT")"
+ done
+ fi
+
+ if "$WIDE" && [[ -n "$detected_cbc_ciphers" ]]; then
+ out "\n "; pr_underline "$(toupper $proto):\n";
+ if "$first"; then
+ neat_header
+ fi
+ first=false
+ for (( i=0; i < nr_ciphers; i++ )); do
+ if "${ciphers_found[i]}" || "$SHOW_EACH_C"; then
+ export="${export2[i]}"
+ neat_list "$(tolower "${normalized_hexcode[i]}")" "${ciph[i]}" "${kx[i]}" "${enc[i]}" "${ciphers_found[i]}"
+ if "$SHOW_EACH_C"; then
+ if "${ciphers_found[i]}"; then
+ if [[ -n "$higher_proto_supported" ]]; then
+ pr_svrty_low "available"
+ else
+ pr_svrty_medium "available"
+ fi
+ else
+ pr_deemphasize "not a/v"
+ fi
+ fi
+ outln "${sigalg[i]}"
+ fi
+ done
+ fi
+
+ if ! "$WIDE"; then
+ if [[ -n "$detected_cbc_ciphers" ]]; then
+ fileout "${jsonID}_CBC_$(toupper $proto)" "MEDIUM" "$detected_cbc_ciphers" "$cve" "$cwe" "$hint"
+ ! "$first" && out "$spaces"
+ out "$(toupper $proto): "
+ [[ -n "$higher_proto_supported" ]] && \
+ pr_svrty_low "$(out_row_aligned_max_width "$detected_cbc_ciphers" " " $TERM_WIDTH)" || \
+ pr_svrty_medium "$(out_row_aligned_max_width "$detected_cbc_ciphers" " " $TERM_WIDTH)"
+ outln
+ detected_cbc_ciphers="" # empty for next round
+ first=false
+ else
+ [[ $proto == tls1 ]] && ! $first && echo -n "$spaces "
+ prln_svrty_good "no CBC ciphers for $(toupper $proto) (OK)"
+ first=false
+ fi
+ else
+ if ! "$vuln_beast" ; then
+ prln_svrty_good "no CBC ciphers for $(toupper $proto) (OK)"
+ fileout "${jsonID}_CBC_$(toupper $proto)" "OK" "No CBC ciphers for $(toupper $proto)" "$cve" "$cwe"
+ fi
+ fi
+ done # for proto in ssl3 tls1
+
+ if "$vuln_beast"; then
+ if [[ -n "$higher_proto_supported" ]]; then
+ if "$WIDE"; then
+ outln; out " "
+ # NOT ok seems too harsh for me if we have TLS >1.0
+ pr_svrty_low "VULNERABLE"
+ outln " -- but also supports higher protocols (possible mitigation) $higher_proto_supported"
+ outln
+ else
+ out "$spaces"
+ pr_svrty_low "VULNERABLE"
+ outln " -- but also supports higher protocols $higher_proto_supported (likely mitigated)"
+ fi
+ fileout "$jsonID" "LOW" "VULNERABLE -- but also supports higher protocols $higher_proto_supported (likely mitigated)" "$cve" "$cwe" "$hint"
+ else
+ if "$WIDE"; then
+ outln
+ else
+ out "$spaces"
+ fi
+ pr_svrty_medium "VULNERABLE"
+ outln " -- and no higher protocols as mitigation supported"
+ fileout "$jsonID" "MEDIUM" "VULNERABLE -- and no higher protocols as mitigation supported" "$cve" "$cwe" "$hint"
+ fi
+ fi
+ "$first" && ! "$vuln_beast" && prln_svrty_good "no CBC ciphers found for any protocol (OK)"
+
+ "$using_sockets" && HAS_DH_BITS="$has_dh_bits"
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 0
+}
+
+
+# https://web.archive.org/web/20200324101422/http://www.isg.rhul.ac.uk/tls/Lucky13.html
+# in a nutshell: don't offer CBC suites (again). MAC as a fix for padding oracles is not enough. Best: TLS v1.2+ AES GCM
+run_lucky13() {
+ local spaces=" "
+ local cbc_ciphers="ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:SRP-DSS-AES-256-CBC-SHA:SRP-RSA-AES-256-CBC-SHA:SRP-AES-256-CBC-SHA:RSA-PSK-AES256-CBC-SHA384:DHE-PSK-AES256-CBC-SHA384:DHE-PSK-AES256-CBC-SHA:ECDHE-PSK-CAMELLIA256-SHA384:RSA-PSK-CAMELLIA256-SHA384:DHE-PSK-CAMELLIA256-SHA384:PSK-AES256-CBC-SHA384:PSK-CAMELLIA256-SHA384:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA256:DH-RSA-AES256-SHA256:DH-DSS-AES256-SHA256:DHE-RSA-AES256-SHA:DHE-DSS-AES256-SHA:DH-RSA-AES256-SHA:DH-DSS-AES256-SHA:ECDHE-RSA-CAMELLIA256-SHA384:ECDHE-ECDSA-CAMELLIA256-SHA384:DHE-RSA-CAMELLIA256-SHA256:DHE-DSS-CAMELLIA256-SHA256:DH-RSA-CAMELLIA256-SHA256:DH-DSS-CAMELLIA256-SHA256:DHE-RSA-CAMELLIA256-SHA:DHE-DSS-CAMELLIA256-SHA:DH-RSA-CAMELLIA256-SHA:DH-DSS-CAMELLIA256-SHA:AECDH-AES256-SHA:ADH-AES256-SHA256:ADH-AES256-SHA:ADH-CAMELLIA256-SHA256:ADH-CAMELLIA256-SHA:ECDH-RSA-AES256-SHA384:ECDH-ECDSA-AES256-SHA384:ECDH-RSA-AES256-SHA:ECDH-ECDSA-AES256-SHA:ECDH-RSA-CAMELLIA256-SHA384:ECDH-ECDSA-CAMELLIA256-SHA384:AES256-SHA256:AES256-SHA:CAMELLIA256-SHA256:ECDHE-PSK-AES256-CBC-SHA384:ECDHE-PSK-AES256-CBC-SHA:CAMELLIA256-SHA:RSA-PSK-AES256-CBC-SHA:PSK-AES256-CBC-SHA:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:SRP-DSS-AES-128-CBC-SHA:SRP-RSA-AES-128-CBC-SHA:SRP-AES-128-CBC-SHA:DHE-RSA-AES128-SHA256:DHE-DSS-AES128-SHA256:DH-RSA-AES128-SHA256:DH-DSS-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA:DH-RSA-AES128-SHA:DH-DSS-AES128-SHA:ECDHE-RSA-CAMELLIA128-SHA256:ECDHE-ECDSA-CAMELLIA128-SHA256:DHE-RSA-CAMELLIA128-SHA256:DHE-DSS-CAMELLIA128-SHA256:DH-RSA-CAMELLIA128-SHA256:DH-DSS-CAMELLIA128-SHA256:DHE-RSA-SEED-SHA:DHE-DSS-SEED-SHA:DH-RSA-SEED-SHA:DH-DSS-SEED-SHA:DHE-RSA-CAMELLIA128-SHA:DHE-DSS-CAMELLIA128-SHA:DH-RSA-CAMELLIA128-SHA:DH-DSS-CAMELLIA128-SHA:AECDH-AES128-SHA:ADH-AES128-SHA256:ADH-AES128-SHA:ADH-CAMELLIA128-SHA256:ADH-SEED-SHA:ADH-CAMELLIA128-SHA:ECDH-RSA-AES128-SHA256:ECDH-ECDSA-AES128-SHA256:ECDH-RSA-AES128-SHA:ECDH-ECDSA-AES128-SHA:ECDH-RSA-CAMELLIA128-SHA256:ECDH-ECDSA-CAMELLIA128-SHA256:AES128-SHA256:AES128-SHA:CAMELLIA128-SHA256:ECDHE-PSK-AES128-CBC-SHA256:ECDHE-PSK-AES128-CBC-SHA:RSA-PSK-AES128-CBC-SHA256:DHE-PSK-AES128-CBC-SHA256:DHE-PSK-AES128-CBC-SHA:SEED-SHA:CAMELLIA128-SHA:ECDHE-PSK-CAMELLIA128-SHA256:RSA-PSK-CAMELLIA128-SHA256:DHE-PSK-CAMELLIA128-SHA256:PSK-AES128-CBC-SHA256:PSK-CAMELLIA128-SHA256:IDEA-CBC-SHA:RSA-PSK-AES128-CBC-SHA:PSK-AES128-CBC-SHA:KRB5-IDEA-CBC-SHA:KRB5-IDEA-CBC-MD5:ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-SHA:SRP-DSS-3DES-EDE-CBC-SHA:SRP-RSA-3DES-EDE-CBC-SHA:SRP-3DES-EDE-CBC-SHA:EDH-RSA-DES-CBC3-SHA:EDH-DSS-DES-CBC3-SHA:DH-RSA-DES-CBC3-SHA:DH-DSS-DES-CBC3-SHA:AECDH-DES-CBC3-SHA:ADH-DES-CBC3-SHA:ECDH-RSA-DES-CBC3-SHA:ECDH-ECDSA-DES-CBC3-SHA:DES-CBC3-SHA:RSA-PSK-3DES-EDE-CBC-SHA:PSK-3DES-EDE-CBC-SHA:KRB5-DES-CBC3-SHA:KRB5-DES-CBC3-MD5:ECDHE-PSK-3DES-EDE-CBC-SHA:DHE-PSK-3DES-EDE-CBC-SHA:EXP1024-DHE-DSS-DES-CBC-SHA:EDH-RSA-DES-CBC-SHA:EDH-DSS-DES-CBC-SHA:DH-RSA-DES-CBC-SHA:DH-DSS-DES-CBC-SHA:ADH-DES-CBC-SHA:EXP1024-DES-CBC-SHA:DES-CBC-SHA:KRB5-DES-CBC-SHA:KRB5-DES-CBC-MD5:EXP-EDH-RSA-DES-CBC-SHA:EXP-EDH-DSS-DES-CBC-SHA:EXP-ADH-DES-CBC-SHA:EXP-DES-CBC-SHA:EXP-RC2-CBC-MD5:EXP-KRB5-RC2-CBC-SHA:EXP-KRB5-DES-CBC-SHA:EXP-KRB5-RC2-CBC-MD5:EXP-KRB5-DES-CBC-MD5:EXP-DH-DSS-DES-CBC-SHA:EXP-DH-RSA-DES-CBC-SHA"
+ cbc_ciphers_hex1="c0,28, c0,24, c0,14, c0,0a, c0,22, c0,21, c0,20, 00,b7, 00,b3, 00,91, c0,9b, c0,99, c0,97, 00,af, c0,95, 00,6b, 00,6a, 00,69, 00,68, 00,39, 00,38, 00,37, 00,36, c0,77, c0,73, 00,c4, 00,c3, 00,c2, 00,c1, 00,88, 00,87, 00,86, 00,85, c0,19, 00,6d, 00,3a, 00,c5, 00,89, c0,2a, c0,26, c0,0f, c0,05, c0,79, c0,75, 00,3d, 00,35, 00,c0, c0,38, c0,36, 00,84, 00,95, 00,8d, c0,3d, c0,3f, c0,41, c0,43, c0,45, c0,47, c0,49, c0,4b, c0,4d, c0,4f, c0,65, c0,67, c0,69, c0,71, c0,27, c0,23, c0,13, c0,09, c0,1f, c0,1e, c0,1d, 00,67, 00,40, 00,3f, 00,3e, 00,33, 00,32, 00,31, 00,30, c0,76, c0,72, 00,be, 00,bd, 00,bc, 00,bb, 00,9a, 00,99, 00,98, 00,97, 00,45, 00,44, 00,43, 00,42, c0,18, 00,6c, 00,34, 00,bf, 00,9b, 00,46, c0,29, c0,25, c0,0e, c0,04, c0,78, c0,74, 00,3c, 00,2f, 00,ba"
+ cbc_ciphers_hex2="c0,37, c0,35, 00,b6, 00,b2, 00,90, 00,96, 00,41, c0,9a, c0,98, c0,96, 00,ae, c0,94, 00,07, 00,94, 00,8c, 00,21, 00,25, c0,3c, c0,3e, c0,40, c0,42, c0,44, c0,46, c0,48, c0,4a, c0,4c, c0,4e, c0,64, c0,66, c0,68, c0,70, c0,12, c0,08, c0,1c, c0,1b, c0,1a, 00,16, 00,13, 00,10, 00,0d, c0,17, 00,1b, c0,0d, c0,03, 00,0a, 00,93, 00,8b, 00,1f, 00,23, c0,34, 00,8f, fe,ff, ff,e0, 00,63, 00,15, 00,12, 00,0f, 00,0c, 00,1a, 00,62, 00,09, 00,61, 00,1e, 00,22, fe,fe, ff,e1, 00,14, 00,11, 00,19, 00,08, 00,06, 00,27, 00,26, 00,2a, 00,29, 00,0b, 00,0e"
+ local has_dh_bits="$HAS_DH_BITS"
+ local -i nr_supported_ciphers=0 sclient_success
+ local using_sockets=true
+ local cve="CVE-2013-0169"
+ local cwe="CWE-310"
+ local hint=""
+ local jsonID="LUCKY13"
+
+ if [[ $VULN_COUNT -le $VULN_THRESHLD ]]; then
+ outln
+ pr_headlineln " Testing for LUCKY13 vulnerability "
+ outln
+ fi
+ pr_bold " LUCKY13"; out " ($cve), experimental "
+
+ if "$TLS13_ONLY"; then
+ pr_svrty_best "not vulnerable (OK)"
+ [[ $DEBUG -ge 1 ]] && out ", no CBC ciphers in TLS 1.3 only servers"
+ outln
+ fileout "$jsonID" "OK" "not vulnerable, TLS 1.3 only" "$cve" "$cwe"
+ return 0
+ fi
+
+ "$SSL_NATIVE" && using_sockets=false
+ # The openssl binary distributed has almost everything we need (PSK, KRB5 ciphers and feff, ffe0 are typically missing).
+ # Measurements show that there's little impact whether we use sockets or TLS here, so the default is sockets here
+
+ if "$using_sockets"; then
+ tls_sockets "03" "${cbc_ciphers_hex1}, 00,ff"
+ sclient_success=$?
+ [[ "$sclient_success" -eq 2 ]] && sclient_success=0
+ if [[ $sclient_success -ne 0 ]]; then
+ tls_sockets "03" "${cbc_ciphers_hex2}, 00,ff"
+ sclient_success=$?
+ [[ $sclient_success -eq 2 ]] && sclient_success=0
+ fi
+ else
+ nr_cbc_ciphers=$(count_ciphers $cbc_ciphers)
+ nr_supported_ciphers=$(count_ciphers $(actually_supported_osslciphers $cbc_ciphers))
+ $OPENSSL s_client $(s_client_options "$STARTTLS $BUGS -no_ssl2 -cipher $cbc_ciphers -connect $NODEIP:$PORT $PROXY $SNI") >$TMPFILE 2>$ERRFILE </dev/null
+ sclient_connect_successful $? $TMPFILE
+ sclient_success=$?
+ [[ "$DEBUG" -eq 2 ]] && grep -Eq "error|failure" $ERRFILE | grep -Eav "unable to get local|verify error"
+ fi
+ if [[ $sclient_success -eq 0 ]]; then
+ out "potentially "
+ pr_svrty_low "VULNERABLE"; out ", uses cipher block chaining (CBC) ciphers with TLS. Check patches"
+ fileout "$jsonID" "LOW" "potentially vulnerable, uses TLS CBC ciphers" "$cve" "$cwe" "$hint"
+ # the CBC padding which led to timing differences during MAC processing has been solved in openssl (https://www.openssl.org/news/secadv/20130205.txt)
+ # and other software. However we can't tell with reasonable effort from the outside. Thus we still issue a warning and label it experimental
+ else
+ pr_svrty_best "not vulnerable (OK)";
+ if "$using_sockets"; then
+ fileout "$jsonID" "OK" "not vulnerable" "$cve" "$cwe"
+ else
+ if [[ "$nr_supported_ciphers" -ge 133 ]]; then
+ # Likely only PSK/KRB5 ciphers are missing: display discrepancy but no warning
+ out ", $nr_supported_ciphers/$nr_cbc_ciphers local ciphers"
+ else
+ pr_warning ", $nr_supported_ciphers/$nr_cbc_ciphers local ciphers"
+ fi
+ fileout "$jsonID" "OK" "not vulnerable ($nr_supported_ciphers of $nr_cbc_ciphers local ciphers" "$cve" "$cwe"
+ fi
+ fi
+ outln
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ [[ $sclient_success -ge 6 ]] && return 1
+ return 0
+}
+
+
+# https://tools.ietf.org/html/rfc7465 REQUIRES that TLS clients and servers NEVER negotiate the use of RC4 cipher suites!
+# https://en.wikipedia.org/wiki/Transport_Layer_Security#RC4_attacks
+# https://blog.cryptographyengineering.com/2013/03/attack-of-week-rc4-is-kind-of-broken-in.html
+#
+run_rc4() {
+ local -i rc4_offered=0
+ local -i nr_ciphers=0 nr_ossl_ciphers=0 nr_nonossl_ciphers=0 sclient_success=0
+ local n auth mac export hexc sslv2_ciphers_hex="" sslv2_ciphers_ossl="" s
+ local -a normalized_hexcode hexcode ciph sslvers kx enc export2 sigalg ossl_supported
+ local -i i
+ local -a ciphers_found ciphers_found2 hexcode2 ciph2 rfc_ciph2
+ local -i -a index
+ local dhlen available="" ciphers_to_test supported_sslv2_ciphers proto
+ local has_dh_bits="$HAS_DH_BITS" rc4_detected=""
+ local using_sockets=true
+ local cve="CVE-2013-2566 CVE-2015-2808"
+ local cwe="CWE-310"
+ local hint=""
+ local jsonID="RC4"
+
+ "$SSL_NATIVE" && using_sockets=false
+ "$FAST" && using_sockets=false
+ [[ $TLS_NR_CIPHERS == 0 ]] && using_sockets=false
+
+ if [[ $VULN_COUNT -le $VULN_THRESHLD ]]; then
+ outln
+ pr_headlineln " Checking for vulnerable RC4 Ciphers "
+ outln
+ fi
+ pr_bold " RC4"; out " (${cve// /, }) "
+
+ if "$TLS13_ONLY"; then
+ pr_svrty_best "not vulnerable (OK)"
+ [[ $DEBUG -ge 1 ]] && out ", no RC4 support in TLS 1.3 only servers"
+ outln
+ fileout "$jsonID" "OK" "not vulnerable, TLS 1.3 only" "$cve" "$cwe"
+ return 0
+ fi
+
+ # Get a list of all the cipher suites to test. #FIXME: This is rather ineffective as RC4 ciphers won't change.
+ # We should instead build a fixed list here like @ other functions
+ if "$using_sockets" || [[ $OSSL_VER_MAJOR -lt 1 ]]; then
+ for (( i=0; i < TLS_NR_CIPHERS; i++ )); do
+ if [[ "${TLS_CIPHER_RFC_NAME[i]}" =~ RC4 ]] && ( "$using_sockets" || "${TLS_CIPHER_OSSL_SUPPORTED[i]}" ); then
+ hexc="$(tolower "${TLS_CIPHER_HEXCODE[i]}")"
+ ciph[nr_ciphers]="${TLS_CIPHER_OSSL_NAME[i]}"
+ rfc_ciph[nr_ciphers]="${TLS_CIPHER_RFC_NAME[i]}"
+ sslvers[nr_ciphers]="${TLS_CIPHER_SSLVERS[i]}"
+ kx[nr_ciphers]="${TLS_CIPHER_KX[i]}"
+ enc[nr_ciphers]="${TLS_CIPHER_ENC[i]}"
+ export2[nr_ciphers]="${TLS_CIPHER_EXPORT[i]}"
+ ciphers_found[nr_ciphers]=false
+ sigalg[nr_ciphers]=""
+ ossl_supported[nr_ciphers]="${TLS_CIPHER_OSSL_SUPPORTED[i]}"
+ if "$using_sockets" && "$WIDE" && ! "$HAS_DH_BITS" &&
+ ( [[ ${kx[nr_ciphers]} == "Kx=ECDH" ]] || [[ ${kx[nr_ciphers]} == "Kx=DH" ]] || [[ ${kx[nr_ciphers]} == "Kx=EDH" ]] ); then
+ ossl_supported[nr_ciphers]=false
+ fi
+ if [[ ${#hexc} -eq 9 ]]; then
+ hexcode[nr_ciphers]="${hexc:2:2},${hexc:7:2}"
+ if [[ "${hexc:2:2}" == 00 ]]; then
+ normalized_hexcode[nr_ciphers]="x${hexc:7:2}"
+ else
+ normalized_hexcode[nr_ciphers]="x${hexc:2:2}${hexc:7:2}"
+ fi
+ else
+ hexcode[nr_ciphers]="${hexc:2:2},${hexc:7:2},${hexc:12:2}"
+ normalized_hexcode[nr_ciphers]="x${hexc:2:2}${hexc:7:2}${hexc:12:2}"
+ sslv2_ciphers_hex+=", ${hexcode[nr_ciphers]}"
+ sslv2_ciphers_ossl+=":${ciph[nr_ciphers]}"
+ fi
+ nr_ciphers+=1
+ fi
+ done
+ else
+ while read hexc n ciph[nr_ciphers] sslvers[nr_ciphers] kx[nr_ciphers] auth enc[nr_ciphers] mac export2[nr_ciphers]; do
+ if [[ "${ciph[nr_ciphers]}" =~ RC4 ]]; then
+ ciphers_found[nr_ciphers]=false
+ if [[ ${#hexc} -eq 9 ]]; then
+ if [[ "${hexc:2:2}" == 00 ]]; then
+ normalized_hexcode[nr_ciphers]="$(tolower "x${hexc:7:2}")"
+ else
+ normalized_hexcode[nr_ciphers]="$(tolower "x${hexc:2:2}${hexc:7:2}")"
+ fi
+ else
+ normalized_hexcode[nr_ciphers]="$(tolower "x${hexc:2:2}${hexc:7:2}${hexc:12:2}")"
+ sslv2_ciphers_ossl+=":${ciph[nr_ciphers]}"
+ fi
+ sigalg[nr_ciphers]=""
+ ossl_supported[nr_ciphers]=true
+ nr_ciphers+=1
+ fi
+ done < <($OPENSSL ciphers $OSSL_CIPHERS_S -V 'ALL:COMPLEMENTOFALL:@STRENGTH' 2>>$ERRFILE)
+ fi
+
+ if "$using_sockets" && [[ -n "$sslv2_ciphers_hex" ]] && [[ $(has_server_protocol ssl2) -ne 1 ]]; then
+ sslv2_sockets "${sslv2_ciphers_hex:2}" "true"
+ if [[ $? -eq 3 ]] && [[ "$V2_HELLO_CIPHERSPEC_LENGTH" -ne 0 ]]; then
+ supported_sslv2_ciphers="$(grep "Supported cipher: " "$TEMPDIR/$NODEIP.parse_sslv2_serverhello.txt")"
+ "$WIDE" && "$SHOW_SIGALGO" && s="$(read_sigalg_from_file "$HOSTCERT")"
+ for (( i=0 ; i<nr_ciphers; i++ )); do
+ if [[ "${sslvers[i]}" == SSLv2 ]] && [[ "$supported_sslv2_ciphers" =~ ${normalized_hexcode[i]} ]]; then
+ ciphers_found[i]=true
+ "$WIDE" && "$SHOW_SIGALGO" && sigalg[i]="$s"
+ rc4_offered=1
+ fi
+ done
+ fi
+ elif "$HAS_SSL2" && [[ -n "$sslv2_ciphers_ossl" ]] && [[ $(has_server_protocol ssl2) -ne 1 ]]; then
+ $OPENSSL s_client -cipher "${sslv2_ciphers_ossl:1}" $STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY -ssl2 >$TMPFILE 2>$ERRFILE </dev/null
+ sclient_connect_successful $? "$TMPFILE"
+ if [[ $? -eq 0 ]]; then
+ supported_sslv2_ciphers="$(grep -A 4 "Ciphers common between both SSL endpoints:" $TMPFILE)"
+ "$WIDE" && "$SHOW_SIGALGO" && s="$(read_sigalg_from_file "$TMPFILE")"
+ for (( i=0 ; i<nr_ciphers; i++ )); do
+ if [[ "${sslvers[i]}" == SSLv2 ]] && [[ "$supported_sslv2_ciphers" =~ ${ciph[i]} ]]; then
+ ciphers_found[i]=true
+ "$WIDE" && "$SHOW_SIGALGO" && sigalg[i]="$s"
+ rc4_offered=1
+ fi
+ done
+ fi
+ fi
+
+ for (( i=0; i < nr_ciphers; i++ )); do
+ if "${ossl_supported[i]}" && [[ "${sslvers[i]}" != SSLv2 ]]; then
+ ciphers_found2[nr_ossl_ciphers]=false
+ ciph2[nr_ossl_ciphers]="${ciph[i]}"
+ index[nr_ossl_ciphers]=$i
+ nr_ossl_ciphers+=1
+ fi
+ done
+
+ for proto in -no_ssl2 -tls1_1 -tls1 -ssl3; do
+ [[ "$proto" != -no_ssl2 ]] && [[ $(has_server_protocol "${proto:1}") -eq 1 ]] && continue
+ ! "$HAS_SSL3" && [[ "$proto" == -ssl3 ]] && continue
+ while true; do
+ ciphers_to_test=""
+ for (( i=0; i < nr_ossl_ciphers; i++ )); do
+ ! "${ciphers_found2[i]}" && ciphers_to_test+=":${ciph2[i]}"
+ done
+ [[ -z "$ciphers_to_test" ]] && break
+ $OPENSSL s_client $(s_client_options "$proto -cipher "${ciphers_to_test:1}" $STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY $SNI") >$TMPFILE 2>$ERRFILE </dev/null
+ sclient_connect_successful $? "$TMPFILE" || break
+ cipher=$(get_cipher $TMPFILE)
+ [[ -z "$cipher" ]] && break
+ for (( i=0; i < nr_ossl_ciphers; i++ )); do
+ [[ "$cipher" == "${ciph2[i]}" ]] && ciphers_found2[i]=true && break
+ done
+ [[ $i -eq $nr_ossl_ciphers ]] && break
+ rc4_offered=1
+ i=${index[i]}
+ ciphers_found[i]=true
+ if "$WIDE" && ( [[ ${kx[i]} == "Kx=ECDH" ]] || [[ ${kx[i]} == "Kx=DH" ]] || [[ ${kx[i]} == "Kx=EDH" ]] ); then
+ dhlen=$(read_dhbits_from_file "$TMPFILE" quiet)
+ kx[i]="${kx[i]} $dhlen"
+ fi
+ "$WIDE" && "$SHOW_SIGALGO" && grep -q "\-\-\-\-\-BEGIN CERTIFICATE\-\-\-\-\-" $TMPFILE && \
+ sigalg[i]="$(read_sigalg_from_file "$TMPFILE")"
+ done
+ done
+
+ if "$using_sockets"; then
+ for (( i=0; i < nr_ciphers; i++ )); do
+ if ! "${ciphers_found[i]}" && [[ "${sslvers[i]}" != SSLv2 ]]; then
+ ciphers_found2[nr_nonossl_ciphers]=false
+ hexcode2[nr_nonossl_ciphers]="${hexcode[i]}"
+ rfc_ciph2[nr_nonossl_ciphers]="${rfc_ciph[i]}"
+ index[nr_nonossl_ciphers]=$i
+ nr_nonossl_ciphers+=1
+ fi
+ done
+ fi
+
+ for proto in 03 02 01 00; do
+ [[ $(has_server_protocol "$proto") -eq 1 ]] && continue
+ while true; do
+ ciphers_to_test=""
+ for (( i=0; i < nr_nonossl_ciphers; i++ )); do
+ ! "${ciphers_found2[i]}" && ciphers_to_test+=", ${hexcode2[i]}"
+ done
+ [[ -z "$ciphers_to_test" ]] && break
+ if "$WIDE" && "$SHOW_SIGALGO"; then
+ tls_sockets "$proto" "${ciphers_to_test:2}, 00,ff" "all"
+ else
+ tls_sockets "$proto" "${ciphers_to_test:2}, 00,ff" "ephemeralkey"
+ fi
+ sclient_success=$?
+ [[ $sclient_success -ne 0 ]] && [[ $sclient_success -ne 2 ]] && break
+ cipher=$(get_cipher "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt")
+ for (( i=0; i < nr_nonossl_ciphers; i++ )); do
+ [[ "$cipher" == "${rfc_ciph2[i]}" ]] && ciphers_found2[i]=true && break
+ done
+ [[ $i -eq $nr_nonossl_ciphers ]] && break
+ rc4_offered=1
+ i=${index[i]}
+ ciphers_found[i]=true
+ if "$WIDE" && ( [[ ${kx[i]} == "Kx=ECDH" ]] || [[ ${kx[i]} == "Kx=DH" ]] || [[ ${kx[i]} == "Kx=EDH" ]] ); then
+ dhlen=$(read_dhbits_from_file "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt" quiet)
+ kx[i]="${kx[i]} $dhlen"
+ fi
+ "$WIDE" && "$SHOW_SIGALGO" && [[ -r "$HOSTCERT" ]] && \
+ sigalg[i]="$(read_sigalg_from_file "$HOSTCERT")"
+ done
+ done
+
+ if [[ $rc4_offered -eq 1 ]]; then
+ "$WIDE" || pr_svrty_high "VULNERABLE (NOT ok): "
+ if "$WIDE"; then
+ outln "\n"
+ neat_header
+ fi
+ for (( i=0 ; i<nr_ciphers; i++ )); do
+ if ! "${ciphers_found[i]}" && ! "$SHOW_EACH_C"; then
+ continue # no successful connect AND not verbose displaying each cipher
+ fi
+ if "$WIDE"; then
+ #FIXME: JSON+CSV in wide mode is missing
+ export="${export2[i]}"
+ neat_list "${normalized_hexcode[i]}" "${ciph[i]}" "${kx[i]}" "${enc[i]}" "${ciphers_found[i]}"
+ if "$SHOW_EACH_C"; then
+ if "${ciphers_found[i]}"; then
+ pr_svrty_high "available"
+ else
+ pr_deemphasize "not a/v"
+ fi
+ fi
+ outln "${sigalg[i]}"
+ fi
+ if "${ciphers_found[i]}"; then
+ if ( [[ "$DISPLAY_CIPHERNAMES" =~ openssl ]] && [[ "${ciph[i]}" != "-" ]] ) || [[ "${rfc_ciph[i]}" == "-" ]]; then
+ rc4_detected+="${ciph[i]} "
+ else
+ rc4_detected+="${rfc_ciph[i]} "
+ fi
+ fi
+ done
+ ! "$WIDE" && pr_svrty_high "$(out_row_aligned_max_width "$rc4_detected" " " $TERM_WIDTH)"
+ outln
+ "$WIDE" && out " " && prln_svrty_high "VULNERABLE (NOT ok)"
+ fileout "$jsonID" "HIGH" "VULNERABLE, Detected ciphers: $rc4_detected" "$cve" "$cwe" "$hint"
+ elif [[ $nr_ciphers -eq 0 ]]; then
+ prln_local_problem "No RC4 Ciphers configured in $OPENSSL"
+ fileout "$jsonID" "WARN" "RC4 ciphers not supported by local OpenSSL ($OPENSSL)"
+ else
+ prln_svrty_good "no RC4 ciphers detected (OK)"
+ fileout "$jsonID" "OK" "not vulnerable" "$cve" "$cwe"
+ fi
+ outln
+
+ "$using_sockets" && HAS_DH_BITS="$has_dh_bits"
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ [[ $sclient_success -ge 6 ]] && return 1
+ return 0
+}
+
+
+run_youknowwho() {
+ local cve="CVE-2013-2566"
+ local cwe="CWE-310"
+ # NOT FIXME as there's no code: https://web.archive.org/web/20191008002003/http://www.isg.rhul.ac.uk/tls/index.html
+ # https://blog.cryptographyengineering.com/2013/03/attack-of-week-rc4-is-kind-of-broken-in.html
+ return 0
+ # in a nutshell: don't use RC4, really not!
+}
+
+ # https://www.usenix.org/conference/woot13/workshop-program/presentation/smyth
+ # https://secure-resumption.com/tlsauth.pdf
+run_tls_truncation() {
+ #FIXME: difficult to test, is there any test available: pls let me know
+ :
+}
+
+# Test for various server implementation errors that aren't tested for elsewhere.
+# Inspired by https://datatracker.ietf.org/doc/draft-ietf-tls-grease.
+run_grease() {
+ local -i success
+ local bug_found=false
+ local normal_hello_ok=false
+ local cipher_list proto selected_cipher selected_cipher_hex="" extn rnd_bytes
+ local alpn_proto alpn alpn_list_len_hex extn_len_hex
+ local selected_alpn_protocol grease_selected_alpn_protocol
+ local ciph list temp curve_found
+ local -i i j rnd alpn_list_len extn_len debug_level=""
+ local -i ret=0
+ # Note: The following values were taken from https://datatracker.ietf.org/doc/draft-ietf-tls-grease.
+ # These arrays may need to be updated if the values change in the final version of this document.
+ local -a -r grease_cipher_suites=( "0a,0a" "1a,1a" "2a,2a" "3a,3a" "4a,4a" "5a,5a" "6a,6a" "7a,7a" "8a,8a" "9a,9a" "aa,aa" "ba,ba" "ca,ca" "da,da" "ea,ea" "fa,fa" )
+ local -a -r grease_supported_groups=( "0a,0a" "1a,1a" "2a,2a" "3a,3a" "4a,4a" "5a,5a" "6a,6a" "7a,7a" "8a,8a" "9a,9a" "aa,aa" "ba,ba" "ca,ca" "da,da" "ea,ea" "fa,fa" )
+ local -a -r grease_extn_values=( "0a,0a" "1a,1a" "2a,2a" "3a,3a" "4a,4a" "5a,5a" "6a,6a" "7a,7a" "8a,8a" "9a,9a" "aa,aa" "ba,ba" "ca,ca" "da,da" "ea,ea" "fa,fa" )
+ local -r ecdhe_ciphers="cc,14, cc,13, c0,30, c0,2c, c0,28, c0,24, c0,14, c0,0a, c0,9b, cc,a9, cc,a8, c0,af, c0,ad, c0,77, c0,73, c0,19, cc,ac, c0,38, c0,36, c0,49, c0,4d, c0,5d, c0,61, c0,71, c0,87, c0,8b, c0,2f, c0,2b, c0,27, c0,23, c0,13, c0,09, c0,ae, c0,ac, c0,76, c0,72, c0,18, c0,37, c0,35, c0,9a, c0,48, c0,4c, c0,5c, c0,60, c0,70, c0,86, c0,8a, c0,11, c0,07, c0,16, c0,33, c0,12, c0,08, c0,17, c0,34, c0,10, c0,06, c0,15, c0,3b, c0,3a, c0,39"
+ local jsonID="GREASE"
+
+ outln; pr_headline " Testing for server implementation bugs "; outln "\n"
+
+ # Many of the following checks work by modifying the "basic" call to
+ # tls_sockets() and assuming the tested-for bug is present if the
+ # connection fails. However, this only works if the connection succeeds
+ # with the "basic" call. So, keep trying different "basic" calls until
+ # one is found that succeeds.
+ for (( i=0; i < 5; i++ )); do
+ case $i in
+ 0) proto="03" ; cipher_list="$TLS12_CIPHER" ;;
+ 2) proto="02" ; cipher_list="$TLS_CIPHER" ;;
+ 3) proto="01" ; cipher_list="$TLS_CIPHER" ;;
+ 4) proto="00" ; cipher_list="$TLS_CIPHER" ;;
+ esac
+ tls_sockets "$proto" "$cipher_list"
+ success=$?
+ if [[ $success -eq 0 ]] || [[ $success -eq 2 ]]; then
+ break
+ fi
+ done
+ if [[ $success -eq 0 ]] || [[ $success -eq 2 ]]; then
+ selected_cipher=$(get_cipher "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt")
+ if [[ $TLS_NR_CIPHERS -ne 0 ]]; then
+ for (( i=0; i < TLS_NR_CIPHERS; i++ )); do
+ [[ "$selected_cipher" == "${TLS_CIPHER_RFC_NAME[i]}" ]] && selected_cipher_hex="${TLS_CIPHER_HEXCODE[i]}" && break
+ done
+ elif "$HAS_SSL2"; then
+ selected_cipher_hex="$($OPENSSL ciphers -V -tls1 'ALL:COMPLEMENTOFALL' | awk '/'" $selected_cipher "'/ { print $1 }')"
+ elif "$HAS_CIPHERSUITES"; then
+ selected_cipher_hex="$($OPENSSL ciphers -V -ciphersuites "$TLS13_OSSL_CIPHERS" 'ALL:COMPLEMENTOFALL'| awk '/'" $selected_cipher "'/ { print $1 }')"
+ else
+ selected_cipher_hex="$($OPENSSL ciphers -V 'ALL:COMPLEMENTOFALL'| awk '/'" $selected_cipher "'/ { print $1 }')"
+ fi
+ if [[ -n "$selected_cipher_hex" ]]; then
+ normal_hello_ok=true
+ selected_cipher_hex="${selected_cipher_hex:2:2},${selected_cipher_hex:7:2}"
+ fi
+ else
+ proto="03"
+ fi
+
+ # Test for yaSSL bug - server only looks at second byte of each cipher
+ # suite listed in ClientHello (see issue #793). First check to see if
+ # server ignores the ciphers in the ClientHello entirely, then check to
+ # see if server only looks at second byte of each offered cipher.
+
+ # Send a list of non-existent ciphers where the second byte does not match
+ # any existing cipher.
+
+ # Need to ensure that $TEMPDIR/$NODEIP.parse_tls_serverhello.txt contains the results of the
+ # most recent calls to tls_sockets even if tls_sockets is not successful. Setting $DEBUG to
+ # a non-zero value ensures this. Setting it to 1 prevents any extra information from being
+ # displayed.
+ debug_level="$DEBUG"
+ [[ $DEBUG -eq 0 ]] && DEBUG=1
+ debugme echo -e "\nSending ClientHello with non-existent ciphers."
+ tls_sockets "$proto" "de,d0, de,d1, d3,d2, de,d3, 00,ff"
+ success=$?
+ if [[ $success -eq 0 ]] || [[ $success -eq 2 ]]; then
+ prln_svrty_medium " Server claims to support non-existent cipher suite."
+ fileout "$jsonID" "MEDIUM" "Server claims to support non-existent cipher suite."
+ bug_found=true
+ elif grep -q "The ServerHello specifies a cipher suite that wasn't included in the ClientHello" "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt" ; then
+ prln_svrty_medium " Server responded with a ServerHello rather than an alert even though it doesn't support any of the client-offered cipher suites."
+ fileout "$jsonID" "MEDIUM" "Server responded with a ServerHello rather than an alert even though it doesn't support any of the client-offered cipher suites."
+ bug_found=true
+ else
+ # Send a list of non-existent ciphers such that for each cipher that
+ # is defined, there is one in the list that matches in the second byte
+ # (but make sure list contains at more 127 ciphers).
+ debugme echo -e "\nSending ClientHello with non-existent ciphers, but that match existing ciphers in second byte."
+ tls_sockets "$proto" "de,01, de,02, de,03, de,04, de,05, de,06, de,07, de,08, de,09, de,0a, de,0b, de,0c, de,0d, de,0e, de,0f, de,10, de,11, de,12, de,13, de,14, de,15, de,16, de,17, de,18, de,19, de,1a, de,1b, de,23, de,24, de,25, de,26, de,27, de,28, de,29, de,2a, de,2b, de,2c, de,2d, de,2e, de,2f, de,30, de,31, de,32, de,33, de,34, de,35, de,36, de,37, de,38, de,39, de,3a, de,3b, de,3c, de,3d, de,3e, de,3f, de,40, de,41, de,42, de,43, de,44, de,45, de,46, de,60, de,61, de,62, de,63, de,64, de,65, de,66, de,67, de,68, de,69, de,6a, de,6b, de,6c, de,6d, de,72, de,73, de,74, de,75, de,76, de,77, de,78, de,79, de,84, de,85, de,86, de,87, de,88, de,89, de,96, de,97, de,98, de,99, de,9a, de,9b, de,9c, de,9d, de,9e, de,9f, de,a0, de,a1, de,a2, de,a3, de,a4, de,a5, de,a6, de,a7, de,ba, de,bb, de,bc, de,bd, de,be, de,bf, de,c0, de,c1, de,c2, de,c3, de,c4, de,c5, 00,ff"
+ success=$?
+ if [[ $success -eq 0 ]] || [[ $success -eq 2 ]]; then
+ prln_svrty_medium " Server claims to support non-existent cipher suite."
+ fileout "$jsonID" "MEDIUM" "Server claims to support non-existent cipher suite."
+ bug_found=true
+ elif grep -q " The ServerHello specifies a cipher suite that wasn't included in the ClientHello" "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt" ; then
+ prln_svrty_medium " Server only compares against second byte in each cipher suite in ClientHello."
+ fileout "$jsonID" "MEDIUM" "Server only compares against second byte in each cipher suite in ClientHello."
+ bug_found=true
+ fi
+ fi
+ DEBUG="$debug_level"
+
+ # Check that server ignores unrecognized extensions
+ # see https://datatracker.ietf.org/doc/draft-ietf-tls-grease
+ if "$normal_hello_ok" && [[ "$proto" != "00" ]]; then
+ # Try multiple different randomly-generated GREASE extensions,
+ # but make final test use zero-length extension value, just to
+ # be sure that works before testing server with a zero-length
+ # extension as the final extension.
+ for (( i=1; i <= 5; i++ )); do
+ # Create a random extension using one of the GREASE values.
+ rnd=$RANDOM%${#grease_extn_values[@]}
+ extn="${grease_extn_values[rnd]}"
+ if [[ $i -eq 5 ]]; then
+ extn_len=0
+ else
+ # Not sure what a good upper bound is here, but a key_share
+ # extension with an ffdhe8192 would be over 1024 bytes.
+ extn_len=$RANDOM%1024
+ fi
+ extn_len_hex=$(printf "%04x" $extn_len)
+ extn+=",${extn_len_hex:0:2},${extn_len_hex:2:2}"
+ for (( j=0; j <= extn_len-2; j=j+2 )); do
+ rnd_bytes="$(printf "%04x" $RANDOM)"
+ extn+=",${rnd_bytes:0:2},${rnd_bytes:2:2}"
+ done
+ if [[ $j -lt $extn_len ]]; then
+ rnd_bytes="$(printf "%04x" $RANDOM)"
+ extn+=",${rnd_bytes:0:2}"
+ fi
+ if [[ $DEBUG -ge 2 ]]; then
+ echo -en "\nSending ClientHello with unrecognized extension"
+ [[ $DEBUG -ge 3 ]] && echo -n ": $extn"
+ echo ""
+ fi
+ tls_sockets "$proto" "$cipher_list" "" "$extn"
+ success=$?
+ if [[ $success -ne 0 ]] && [[ $success -ne 2 ]]; then
+ break
+ fi
+ done
+ if [[ $success -ne 0 ]] && [[ $success -ne 2 ]]; then
+ prln_svrty_medium " Server fails if ClientHello contains an unrecognized extension."
+ outln " extension used in failed test: $extn"
+ fileout "$jsonID" "MEDIUM" "Server fails if ClientHello contains an unrecognized extension: $extn"
+ bug_found=true
+ else
+ # Check for inability to handle empty last extension (see PR #792 and
+ # https://www.ietf.org/mail-archive/web/tls/current/msg19720.html).
+ # (Since this test also uses an unrecognized extension, only run this
+ # test if the previous test passed, and use the final extension value
+ # from that test to ensure that the only difference is the location
+ # of the extension.)
+
+ # The "extra extensions" parameter needs to include the padding and
+ # heartbeat extensions, since otherwise prepare_tls_clienthello()
+ # will add these extensions to the end of the ClientHello.
+ debugme echo -e "\nSending ClientHello with empty last extension."
+ tls_sockets "$proto" "$cipher_list" "" "
+ 00,0f, 00,01, 01,
+ 00,15, 00,56,
+ 00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,
+ 00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,
+ 00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,
+ 00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,
+ $extn"
+ success=$?
+ if [[ $success -ne 0 ]] && [[ $success -ne 2 ]]; then
+ prln_svrty_medium " Server fails if last extension in ClientHello is empty."
+ fileout "$jsonID" "MEDIUM" "Server fails if last extension in ClientHello is empty."
+ bug_found=true
+ fi
+ fi
+ fi
+
+ # Check for SERVER_SIZE_LIMIT_BUG.
+ # Send a ClientHello with 129 cipher suites (including 0x00,0xff) to see
+ # if adding a 129th cipher to the list causes a failure.
+#TODO: we need to clarify whether the mit is hit at 128 or 129 ciphers.
+ if "$normal_hello_ok" && [[ "$proto" == 03 ]]; then
+ debugme echo -e "\nSending ClientHello with 129 cipher suites."
+ tls_sockets "$proto" "00,27, $cipher_list"
+ success=$?
+ if [[ $success -ne 0 ]] && [[ $success -ne 2 ]]; then
+ prln_svrty_medium " Server fails if ClientHello includes more than 128 cipher suites."
+ fileout "$jsonID" "MEDIUM" "Server fails if ClientHello includes more than 128 cipher suites."
+ SERVER_SIZE_LIMIT_BUG=true
+ bug_found=true
+ fi
+ fi
+
+ # Check for ClientHello size bug. According to RFC 7586 "at least one TLS
+ # implementation is known to hang the connection when [a] ClientHello
+ # record [with a length between 256 and 511 bytes] is received."
+ # If the length of the host name is more than 75 bytes (which would make
+ # $SNI more than 87 bytes), then the ClientHello would be more than 511
+ # bytes if the server_name extension were included. Removing the SNI
+ # extension, however, may not be an option, since the server may reject the
+ # connection attempt for that reason.
+ if "$normal_hello_ok" && [[ "$proto" != 00 ]] && [[ ${#SNI} -le 87 ]]; then
+ # Normally prepare_tls_clienthello() will add a padding extension with a length
+ # that will make the ClientHello be 512 bytes in length. Providing an "extra
+ # extensions" parameter with a short padding extension prevents that.
+ debugme echo -e "\nSending ClientHello with length between 256 and 511 bytes."
+ tls_sockets "$proto" "$cipher_list" "" "00,15,00,01,00"
+ success=$?
+ if [[ $success -ne 0 ]] && [[ $success -ne 2 ]]; then
+ prln_svrty_medium " Server fails if ClientHello is between 256 and 511 bytes in length."
+ fileout "$jsonID" "MEDIUM" "Server fails if ClientHello is between 256 and 511 bytes in length."
+ bug_found=true
+ fi
+ fi
+
+ # Check that server ignores unrecognized cipher suite values
+ # see https://datatracker.ietf.org/doc/draft-ietf-tls-grease
+ if "$normal_hello_ok"; then
+ list=""
+ for ciph in "${grease_cipher_suites[@]}"; do
+ list+=", $ciph"
+ done
+ debugme echo -e "\nSending ClientHello with unrecognized cipher suite values."
+ tls_sockets "$proto" "${list:2}, $selected_cipher_hex, 00,ff"
+ success=$?
+ if [[ $success -ne 0 ]] && [[ $success -ne 2 ]]; then
+ prln_svrty_medium " Server fails if ClientHello contains unrecognized cipher suite values."
+ fileout "$jsonID" "MEDIUM" "Server fails if ClientHello contains unrecognized cipher suite values."
+ bug_found=true
+ fi
+ fi
+
+ # Check that servers that support ECDHE cipher suites ignore
+ # unrecognized named group values.
+ # see https://datatracker.ietf.org/doc/draft-ietf-tls-grease
+ if [[ "$proto" != "00" ]]; then
+ # Send a ClientHello that lists all of the ECDHE cipher suites
+ tls_sockets "$proto" "$ecdhe_ciphers, 00,ff" "ephemeralkey"
+ success=$?
+ if [[ $success -eq 0 ]] || [[ $success -eq 2 ]]; then
+ # Send the same ClientHello as before but with an unrecognized
+ # named group value added. Make the unrecognized value the first
+ # one in the list replacing one of the values in the original list,
+ # but don't replace the value that was selected by the server.
+ rnd=$RANDOM%${#grease_supported_groups[@]}
+ temp=$(awk -F': ' '/^Server Temp Key/ { print $2 }' "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt")
+ curve_found="${temp%%,*}"
+ if [[ "$curve_found" == "ECDH" ]]; then
+ curve_found="${temp#*, }"
+ curve_found="${curve_found%%,*}"
+ fi
+ if [[ "$curve_found" == "B-571" ]]; then
+ extn="
+ 00, 0a, # Type: Supported Elliptic Curves , see RFC 4492
+ 00, 3e, 00, 3c, # lengths
+ ${grease_supported_groups[rnd]}, 00, 0e, 00, 19, 00, 1c, 00, 1e, 00, 0b, 00, 0c, 00, 1b,
+ 00, 18, 00, 09, 00, 0a, 00, 1a, 00, 16, 00, 17, 00, 1d, 00, 08,
+ 00, 06, 00, 07, 00, 14, 00, 15, 00, 04, 00, 05, 00, 12, 00, 13,
+ 00, 01, 00, 02, 00, 03, 00, 0f, 00, 10, 00, 11"
+ else
+ extn="
+ 00, 0a, # Type: Supported Elliptic Curves , see RFC 4492
+ 00, 3e, 00, 3c, # lengths
+ ${grease_supported_groups[rnd]}, 00, 0d, 00, 19, 00, 1c, 00, 1e, 00, 0b, 00, 0c, 00, 1b,
+ 00, 18, 00, 09, 00, 0a, 00, 1a, 00, 16, 00, 17, 00, 1d, 00, 08,
+ 00, 06, 00, 07, 00, 14, 00, 15, 00, 04, 00, 05, 00, 12, 00, 13,
+ 00, 01, 00, 02, 00, 03, 00, 0f, 00, 10, 00, 11"
+ fi
+ debugme echo -e "\nSending ClientHello with unrecognized named group value in supported_groups extension."
+ tls_sockets "$proto" "$ecdhe_ciphers, 00,ff" "" "$extn"
+ success=$?
+ if [[ $success -ne 0 ]] && [[ $success -ne 2 ]]; then
+ prln_svrty_medium " Server fails if ClientHello contains a supported_groups extension with an unrecognized named group value (${grease_supported_groups[rnd]})."
+ fileout "$jsonID" "MEDIUM" "Server fails if ClientHello contains a supported_groups extension with an unrecognized named group value (${grease_supported_groups[rnd]})."
+ bug_found=true
+ fi
+ fi
+ fi
+
+ # Check that servers that support the ALPN extension ignore
+ # unrecognized ALPN values.
+ # see https://datatracker.ietf.org/doc/draft-ietf-tls-grease
+ if "$normal_hello_ok" && [[ -z $STARTTLS ]] && [[ "$proto" != "00" ]]; then
+ for alpn_proto in $ALPN_PROTOs; do
+ alpn+=",$(printf "%02x" ${#alpn_proto}),$(string_to_asciihex "$alpn_proto")"
+ done
+ alpn_list_len=${#alpn}/3
+ alpn_list_len_hex=$(printf "%04x" $alpn_list_len)
+ extn_len=$alpn_list_len+2
+ extn_len_hex=$(printf "%04x" $extn_len)
+ tls_sockets "$proto" "$cipher_list" "all" "00,10,${extn_len_hex:0:2},${extn_len_hex:2:2},${alpn_list_len_hex:0:2},${alpn_list_len_hex:2:2}$alpn"
+ success=$?
+ if [[ $success -ne 0 ]] && [[ $success -ne 2 ]]; then
+ prln_svrty_medium " Server fails if ClientHello contains an application_layer_protocol_negotiation extension."
+ fileout "$jsonID" "MEDIUM" "Server fails if ClientHello contains an application_layer_protocol_negotiation extension."
+ bug_found=true
+ else
+ selected_alpn_protocol="$(grep "ALPN protocol:" "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt" | sed 's/ALPN protocol: //')"
+ # If using a "normal" ALPN extension worked, then add an unrecognized
+ # ALPN value to the beginning of the extension and try again.
+ alpn_proto="ignore/$selected_alpn_protocol"
+ alpn=",$(printf "%02x" ${#alpn_proto}),$(string_to_asciihex "$alpn_proto")$alpn"
+ alpn_list_len=${#alpn}/3
+ alpn_list_len_hex=$(printf "%04x" $alpn_list_len)
+ extn_len=$alpn_list_len+2
+ extn_len_hex=$(printf "%04x" $extn_len)
+ debugme echo -e "\nSending ClientHello with unrecognized ALPN value in application_layer_protocol_negotiation extension."
+ tls_sockets "$proto" "$cipher_list" "all" "00,10,${extn_len_hex:0:2},${extn_len_hex:2:2},${alpn_list_len_hex:0:2},${alpn_list_len_hex:2:2}$alpn"
+ success=$?
+ if [[ $success -ne 0 ]] && [[ $success -ne 2 ]]; then
+ prln_svrty_medium " Server fails if ClientHello contains an application_layer_protocol_negotiation extension with an unrecognized ALPN value."
+ fileout "$jsonID" "MEDIUM" "erver fails if ClientHello contains an application_layer_protocol_negotiation extension with an unrecognized ALPN value."
+ bug_found=true
+ else
+ grease_selected_alpn_protocol="$(grep "ALPN protocol:" "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt" | sed 's/ALPN protocol: //')"
+ if [[ -z "$grease_selected_alpn_protocol" ]] && [[ -n "$selected_alpn_protocol" ]]; then
+ prln_svrty_medium " Server did not ignore unrecognized ALPN value in the application_layer_protocol_negotiation extension."
+ fileout "$jsonID" "MEDIUM" "Server did not ignore unrecognized ALPN value in the application_layer_protocol_negotiation extension."
+ bug_found=true
+ elif [[ "$grease_selected_alpn_protocol" =~ ignore/ ]]; then
+ prln_svrty_medium " Server selected \"ignore/\" ALPN value in the application_layer_protocol_negotiation extension."
+ fileout "$jsonID" "MEDIUM" "Server selected \"ignore/\" ALPN value in the application_layer_protocol_negotiation extension."
+ bug_found=true
+ fi
+ fi
+ fi
+ fi
+
+ # TODO: For servers that support TLSv1.3, check that servers ignore
+ # an unrecognized named group value along with a corresponding
+ # unrecognized key share
+ # see https://www.ietf.org/mail-archive/web/tls/current/msg22322.html
+ # and https://www.ietf.org/mail-archive/web/tls/current/msg22319.html
+
+ # TODO: For servers that support TLSv1.3, check that servers ignore unrecognized
+ # values in the supported_versions extension.
+ # see https://www.ietf.org/mail-archive/web/tls/current/msg22319.html
+
+ # TODO: For servers that support TLSv1.3, check that servers don't require the
+ # psk_key_exchange_modes extension to be present in the ClientHello.
+
+ if ! "$bug_found"; then
+ outln " No bugs found."
+ fileout "$jsonID" "OK" "No bugs found."
+ #return 0
+ else
+ #return 1
+ :
+ fi
+ return $ret
+ #FIXME: No client side error cases where we want to return 1?
+}
+
+# If the server supports any non-PSK cipher suites that use RSA key transport,
+# check if the server is vulnerable to Bleichenbacher's Oracle Threat (ROBOT) attacks.
+# See "Return Of Bleichenbacher's Oracle Threat (ROBOT)" by Hanno Böck,
+# Juraj Somorovsky, and Craig Young (https://robotattack.org).
+#
+run_robot() {
+ local tls_hexcode="03"
+ # A list of all non-PSK cipher suites that use RSA key transport
+ local cipherlist="00,9d, c0,a1, c0,9d, 00,3d, 00,35, 00,c0, 00,84, c0,3d, c0,51, c0,7b, ff,00, ff,01, ff,02, ff,03, c0,a0, c0,9c, 00,9c, 00,3c, 00,2f, 00,ba, 00,96, 00,41, 00,07, c0,3c, c0,50, c0,7a, 00,05, 00,04, 00,0a, fe,ff, ff,e0, 00,62, 00,09, 00,61, fe,fe, ff,e1, 00,64, 00,60, 00,08, 00,06, 00,03, 00,3b, 00,02, 00,01"
+ # A list of all non-PSK cipher suites that use RSA key transport and that use AES in either GCM or CBC mode.
+ local aes_gcm_cbc_cipherlist="00,9d, 00,9c, 00,3d, 00,35, 00,3c, 00,2f"
+ local padded_pms encrypted_pms cke_prefix client_key_exchange rnd_pad
+ local rnd_pms="aa112233445566778899112233445566778899112233445566778899112233445566778899112233445566778899"
+ local change_cipher_spec finished resp
+ local -a response
+ local -i i subret len iteration testnum pubkeybytes
+ local pubkeybits
+ local vulnerable=false send_ccs_finished=true
+ local -i start_time end_time robottimeout=$MAX_WAITSOCK
+ local cve="CVE-2017-17382 CVE-2017-17427 CVE-2017-17428 CVE-2017-13098 CVE-2017-1000385 CVE-2017-13099 CVE-2016-6883 CVE-2012-5081 CVE-2017-6168"
+ local cwe="CWE-203"
+ local jsonID="ROBOT"
+
+ [[ $VULN_COUNT -le $VULN_THRESHLD ]] && outln && pr_headlineln " Testing for Return of Bleichenbacher's Oracle Threat (ROBOT) vulnerability " && outln
+ pr_bold " ROBOT "
+
+ if ( [[ "$STARTTLS_PROTOCOL" =~ ldap ]] || [[ "$STARTTLS_PROTOCOL" =~ irc ]] ); then
+ prln_local_problem "STARTTLS/$STARTTLS_PROTOCOL and --ssl-native collide here"
+ return 1
+ fi
+
+ if [[ ! "$HAS_PKUTIL" ]]; then
+ prln_local_problem "Your $OPENSSL does not support the pkeyutl utility."
+ fileout "$jsonID" "WARN" "$OPENSSL does not support the pkeyutl utility." "$cve" "$cwe"
+ return 1
+ elif ! "$HAS_PKEY"; then
+ prln_local_problem "Your $OPENSSL does not support the pkey utility."
+ fileout "$jsonID" "WARN" "$OPENSSL does not support the pkey utility." "$cve" "$cwe"
+ return 1
+ fi
+
+ if [[ 0 -eq $(has_server_protocol tls1_2) ]]; then
+ tls_hexcode="03"
+ elif [[ 0 -eq $(has_server_protocol tls1_1) ]]; then
+ tls_hexcode="02"
+ elif [[ 0 -eq $(has_server_protocol tls1) ]]; then
+ tls_hexcode="01"
+ elif [[ 0 -eq $(has_server_protocol ssl3) ]]; then
+ tls_hexcode="00"
+ fi
+
+ # Some hosts are only vulnerable with GCM. First send a list of
+ # ciphers that use AES in GCM or CBC mode, with the GCM ciphers
+ # listed first, and then try all ciphers that use RSA key transport
+ # if there is no connection on the first try.
+ tls_sockets "$tls_hexcode" "$aes_gcm_cbc_cipherlist, 00,ff"
+ subret=$?
+ if [[ $subret -eq 0 ]] || [[ $subret -eq 2 ]]; then
+ cipherlist="$aes_gcm_cbc_cipherlist"
+ tls_hexcode="${DETECTED_TLS_VERSION:2:2}"
+ else
+ if [[ "$tls_hexcode" != "03" ]]; then
+ cipherlist="$(strip_inconsistent_ciphers "$tls_hexcode" ", $cipherlist")"
+ cipherlist="${cipherlist:2}"
+ fi
+ tls_sockets "$tls_hexcode" "$cipherlist, 00,ff"
+ subret=$?
+ if [[ $subret -eq 2 ]]; then
+ tls_hexcode="${DETECTED_TLS_VERSION:2:2}"
+ cipherlist="$(strip_inconsistent_ciphers "$tls_hexcode" ", $cipherlist")"
+ cipherlist="${cipherlist:2}"
+ elif [[ $subret -ne 0 ]]; then
+ prln_svrty_best "Server does not support any cipher suites that use RSA key transport"
+ fileout "$jsonID" "OK" "not vulnerable, no RSA key transport cipher" "$cve" "$cwe"
+ return 0
+ fi
+ fi
+
+ # Run the tests in two iterations. In iteration 0, send 5 different client
+ # key exchange (CKE) messages followed by change cipher spec (CCS) and
+ # Finished messages, and check whether the server provided the same
+ # response in each case. If the server didn't provide the same response
+ # for all five messages in iteration 0, then it is vulnerable. Otherwise
+ # try a second time (iteration 1) with the same CKE messages, but without
+ # sending the CCS or Finished messages.
+ # Iterations 0 and 1 are run with a short timeout waiting for the server
+ # to respond to the CKE message. If the server was found to be potentially
+ # vulnerable in iteration 0 or 1 and testssl.sh timed out waiting for a
+ # response in some cases, then retry the test using a longer timeout value.
+ for (( iteration=0; iteration < 3; iteration++ )); do
+ if [[ $iteration -eq 1 ]]; then
+ # If the server was found to be vulnerable in iteration 0, then
+ # there's no need to try the alternative message flow.
+ "$vulnerable" && continue
+ send_ccs_finished=false
+ elif [[ $iteration -eq 2 ]]; then
+ # The tests are being rerun, so reset the vulnerable flag.
+ vulnerable=false
+ fi
+ for (( testnum=0; testnum < 5; testnum++ )); do
+ response[testnum]="untested"
+ done
+ for (( testnum=0; testnum < 5; testnum++ )); do
+ tls_sockets "$tls_hexcode" "$cipherlist, 00,ff" "all" "" "" "false"
+
+ # Create the padded premaster secret to encrypt. The padding should be
+ # of the form "00 02 <random> 00 <TLS version> <premaster secret>."
+ # However, for each test except testnum=0 the padding will be
+ # made incorrect in some way, as specified below.
+
+ # Determine the length of the public key and create the <random> bytes.
+ # <random> should be a length that makes total length of $padded_pms
+ # the same as the length of the public key. <random> should contain no 00 bytes.
+ pubkeybits="$($OPENSSL x509 -noout -pubkey -in $HOSTCERT 2>>$ERRFILE | \
+ $OPENSSL pkey -pubin -text_pub 2>>$ERRFILE | awk -F'(' '/Public-Key/ { print $2 }')"
+ pubkeybits="${pubkeybits%%bit*}"
+ pubkeybytes=$pubkeybits/8
+ [[ $((pubkeybits%8)) -ne 0 ]] && pubkeybytes+=1
+ rnd_pad=""
+ for (( len=0; len < pubkeybytes-52; len=len+2 )); do
+ rnd_pad+="abcd"
+ done
+ [[ $len -eq $pubkeybytes-52 ]] && rnd_pad+="ab"
+
+ case "$testnum" in
+ # correct padding
+ 0) padded_pms="0002${rnd_pad}00${DETECTED_TLS_VERSION}${rnd_pms}" ;;
+ # wrong first two bytes
+ 1) padded_pms="4117${rnd_pad}00${DETECTED_TLS_VERSION}${rnd_pms}" ;;
+ # 0x00 on a wrong position
+ 2) padded_pms="0002${rnd_pad}11${rnd_pms}0011" ;;
+ # no 0x00 in the middle
+ 3) padded_pms="0002${rnd_pad}111111${rnd_pms}" ;;
+ # wrong version number (according to Klima / Pokorny / Rosa paper)
+ 4) padded_pms="0002${rnd_pad}000202${rnd_pms}" ;;
+ esac
+
+ # Encrypt the padded premaster secret using the server's public key.
+ encrypted_pms="$(asciihex_to_binary "$padded_pms" | \
+ $OPENSSL pkeyutl -encrypt -certin -inkey $HOSTCERT -pkeyopt rsa_padding_mode:none 2>/dev/null | \
+ hexdump -v -e '16/1 "%02x"')"
+ if [[ -z "$encrypted_pms" ]]; then
+ if [[ "$DETECTED_TLS_VERSION" == "0300" ]]; then
+ socksend ",x15, x03, x00, x00, x02, x02, x00" 0
+ else
+ socksend ",x15, x03, x01, x00, x02, x02, x00" 0
+ fi
+ close_socket
+ prln_fixme "Conversion of public key failed around line $((LINENO - 9))"
+ fileout "$jsonID" "WARN" "Conversion of public key failed around line $((LINENO - 10)) "
+ return 1
+ fi
+
+ # Create the client key exchange message.
+ len=${#encrypted_pms}/2
+ cke_prefix="16${DETECTED_TLS_VERSION}$(printf "%04x" $((len+6)))10$(printf "%06x" $((len+2)))$(printf "%04x" $len)"
+ encrypted_pms="$cke_prefix$encrypted_pms"
+ len=${#encrypted_pms}
+ client_key_exchange=""
+ for (( i=0; i<len; i=i+2 )); do
+ client_key_exchange+=", x${encrypted_pms:i:2}"
+ done
+
+ # The contents of change cipher spec are fixed.
+ change_cipher_spec=", x14, x${DETECTED_TLS_VERSION:0:2}, x${DETECTED_TLS_VERSION:2:2}, x00, x01, x01"
+
+ # Send an arbitrary Finished message.
+ finished=", x16, x${DETECTED_TLS_VERSION:0:2}, x${DETECTED_TLS_VERSION:2:2}
+ , x00, x40, x6e, x49, x65, x68, x00, x46, x79, xfd, x5a, x57, xdc
+ , x3e, xef, xb2, xd2, xac, xe0, x8c, x54, x2d, x5f, x00, x87, xdb
+ , xb6, xe3, x77, x2c, x9d, x88, x27, x38, x98, x7d, xcd, x7e, xac
+ , xdd, x5d, x72, xbe, x24, x0d, x20, x36, x14, x0e, x94, x51, xde
+ , xa0, xb6, xc7, x56, x28, xd8, xa1, xcb, x24, xb9, x03, xd0, x7c, x50"
+
+ if "$send_ccs_finished"; then
+ debugme echo -en "\nsending client key exchange, change cipher spec, finished... "
+ socksend "$client_key_exchange$change_cipher_spec$finished" $USLEEP_SND
+ else
+ debugme echo -en "\nsending client key exchange... "
+ socksend "$client_key_exchange" $USLEEP_SND
+ fi
+ debugme echo "reading server error response..."
+ start_time=$(LC_ALL=C date "+%s")
+ sockread_serverhello 32768 $robottimeout
+ subret=$?
+ if [[ $subret -eq 0 ]]; then
+ end_time=$(LC_ALL=C date "+%s")
+ resp=$(hexdump -v -e '16/1 "%02x"' "$SOCK_REPLY_FILE")
+ response[testnum]="${resp%%[!0-9A-F]*}"
+ # The first time a response is received to a client key
+ # exchange message, measure the amount of time it took to
+ # receive a response and set the timeout value for future
+ # tests to 2 seconds longer than it took to receive a response.
+ [[ $iteration -ne 2 ]] && [[ $robottimeout -eq $MAX_WAITSOCK ]] && \
+ [[ $((end_time-start_time)) -lt $((MAX_WAITSOCK-2)) ]] && \
+ robottimeout=$((end_time-start_time+2))
+ else
+ response[testnum]="Timeout waiting for alert"
+ fi
+ debugme echo -e "\nresponse[$testnum] = ${response[testnum]}"
+ [[ $DEBUG -ge 3 ]] && [[ $subret -eq 0 ]] && parse_tls_serverhello "${response[testnum]}"
+ close_socket
+
+ # Don't continue testing if it has already been determined that
+ # tests need to be rerun with a longer timeout.
+ if [[ $iteration -ne 2 ]]; then
+ for (( i=1; i <= testnum; i++ )); do
+ if [[ "${response[i]}" != "${response[$((i-1))]}" ]] && \
+ ( [[ "${response[i]}" == "Timeout waiting for alert" ]] || \
+ [[ "${response[$((i-1))]}" == "Timeout waiting for alert" ]] ); then
+ vulnerable=true
+ break
+ fi
+ done
+ "$vulnerable" && break
+ fi
+ # Don't continue testing if it has already been determined that the server is
+ # strongly vulnerable.
+ if [[ $testnum -eq 2 ]]; then
+ [[ "${response[1]}" != "${response[2]}" ]] && break
+ elif [[ $testnum -eq 3 ]]; then
+ [[ "${response[2]}" != "${response[3]}" ]] && break
+ [[ "${response[0]}" != "${response[1]}" ]] && break
+ fi
+ done
+ # If the server provided the same error message for all tests, then this
+ # is an indication that the server is not vulnerable.
+ if [[ "${response[0]}" != "${response[1]}" ]] || [[ "${response[1]}" != "${response[2]}" ]] || \
+ [[ "${response[2]}" != "${response[3]}" ]] || [[ "${response[3]}" != "${response[4]}" ]]; then
+ vulnerable=true
+
+ # If the test was run with a short timeout and was found to be
+ # potentially vulnerable due to some tests timing out, then
+ # verify the results by rerunning with a longer timeout.
+ if [[ $robottimeout -eq $MAX_WAITSOCK ]]; then
+ break
+ elif [[ "${response[0]}" == "Timeout waiting for alert" ]] || \
+ [[ "${response[1]}" == "Timeout waiting for alert" ]] || \
+ [[ "${response[2]}" == "Timeout waiting for alert" ]] || \
+ [[ "${response[3]}" == "Timeout waiting for alert" ]] || \
+ [[ "${response[4]}" == "Timeout waiting for alert" ]]; then
+ robottimeout=10
+ else
+ break
+ fi
+ fi
+ ! "$vulnerable" && [[ $iteration -eq 1 ]] && break
+ done
+
+ if "$vulnerable"; then
+ if [[ "${response[1]}" == "${response[2]}" ]] && [[ "${response[2]}" == "${response[3]}" ]]; then
+ pr_svrty_medium "VULNERABLE (NOT ok)"; outln " - weakly vulnerable as the attack would take too long"
+ fileout "$jsonID" "MEDIUM" "VULNERABLE, but the attack would take too long" "$cve" "$cwe"
+ else
+ prln_svrty_critical "VULNERABLE (NOT ok)"
+ fileout "$jsonID" "CRITICAL" "VULNERABLE" "$cve" "$cwe"
+ fi
+ else
+ prln_svrty_best "not vulnerable (OK)"
+ fileout "$jsonID" "OK" "not vulnerable" "$cve" "$cwe"
+ fi
+ return 0
+}
+
+old_fart() {
+ out "Get precompiled bins or compile "
+ pr_url "https://github.com/PeterMosmans/openssl"
+ outln "."
+ fileout_insert_warning "old_fart" "WARN" "Your $OPENSSL $OSSL_VER version is an old fart... . It doesn\'t make much sense to proceed. Get precompiled bins or compile https://github.com/PeterMosmans/openssl ."
+ fatal "Your $OPENSSL $OSSL_VER version is an old fart... . It doesn't make much sense to proceed." $ERR_OSSLBIN
+}
+
+# try very hard to determine the install path to get ahold of the mapping file and the CA bundles
+# TESTSSL_INSTALL_DIR can be supplied via environment so that the cipher mapping and CA bundles can be found
+# www.carbonwind.net/TLS_Cipher_Suites_Project/tls_ssl_cipher_suites_simple_table_all.htm
+get_install_dir() {
+ [[ -z "$TESTSSL_INSTALL_DIR" ]] && TESTSSL_INSTALL_DIR="$(dirname "${BASH_SOURCE[0]}")"
+
+ if [[ -r "$RUN_DIR/etc/cipher-mapping.txt" ]]; then
+ CIPHERS_BY_STRENGTH_FILE="$RUN_DIR/etc/cipher-mapping.txt"
+ [[ -z "$TESTSSL_INSTALL_DIR" ]] && TESTSSL_INSTALL_DIR="$RUN_DIR" # probably TESTSSL_INSTALL_DIR
+ fi
+
+ [[ -r "$TESTSSL_INSTALL_DIR/etc/cipher-mapping.txt" ]] && CIPHERS_BY_STRENGTH_FILE="$TESTSSL_INSTALL_DIR/etc/cipher-mapping.txt"
+ if [[ ! -r "$CIPHERS_BY_STRENGTH_FILE" ]]; then
+ [[ -r "$RUN_DIR/cipher-mapping.txt" ]] && CIPHERS_BY_STRENGTH_FILE="$RUN_DIR/cipher-mapping.txt"
+ [[ -r "$TESTSSL_INSTALL_DIR/cipher-mapping.txt" ]] && CIPHERS_BY_STRENGTH_FILE="$TESTSSL_INSTALL_DIR/cipher-mapping.txt"
+ fi
+
+ # we haven't found the cipher file yet...
+ if [[ ! -r "$CIPHERS_BY_STRENGTH_FILE" ]] && type -p readlink &>/dev/null ; then
+ readlink -f ls &>/dev/null && \
+ TESTSSL_INSTALL_DIR="$(readlink -f "$(basename "${BASH_SOURCE[0]}")")" || \
+ TESTSSL_INSTALL_DIR="$(readlink "$(basename "${BASH_SOURCE[0]}")")"
+ # not sure whether Darwin has -f
+ TESTSSL_INSTALL_DIR="$(dirname "$TESTSSL_INSTALL_DIR" 2>/dev/null)"
+ [[ -r "$TESTSSL_INSTALL_DIR/cipher-mapping.txt" ]] && CIPHERS_BY_STRENGTH_FILE="$TESTSSL_INSTALL_DIR/cipher-mapping.txt"
+ [[ -r "$TESTSSL_INSTALL_DIR/etc/cipher-mapping.txt" ]] && CIPHERS_BY_STRENGTH_FILE="$TESTSSL_INSTALL_DIR/etc/cipher-mapping.txt"
+ fi
+
+ # still no cipher mapping file:
+ if [[ ! -r "$CIPHERS_BY_STRENGTH_FILE" ]] && type -p realpath &>/dev/null ; then
+ TESTSSL_INSTALL_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")"
+ CIPHERS_BY_STRENGTH_FILE="$TESTSSL_INSTALL_DIR/etc/cipher-mapping.txt"
+ [[ -r "$TESTSSL_INSTALL_DIR/cipher-mapping.txt" ]] && CIPHERS_BY_STRENGTH_FILE="$TESTSSL_INSTALL_DIR/cipher-mapping.txt"
+ fi
+
+ # still no cipher mapping file (and realpath is not present):
+ if [[ ! -r "$CIPHERS_BY_STRENGTH_FILE" ]] && type -p readlink &>/dev/null ; then
+ readlink -f ls &>/dev/null && \
+ TESTSSL_INSTALL_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" || \
+ TESTSSL_INSTALL_DIR="$(dirname "$(readlink "${BASH_SOURCE[0]}")")"
+ # not sure whether Darwin has -f
+ CIPHERS_BY_STRENGTH_FILE="$TESTSSL_INSTALL_DIR/etc/cipher-mapping.txt"
+ [[ -r "$TESTSSL_INSTALL_DIR/cipher-mapping.txt" ]] && CIPHERS_BY_STRENGTH_FILE="$TESTSSL_INSTALL_DIR/cipher-mapping.txt"
+ fi
+
+ if [[ ! -r "$CIPHERS_BY_STRENGTH_FILE" ]]; then
+ DISPLAY_CIPHERNAMES="openssl-only"
+ debugme echo "$CIPHERS_BY_STRENGTH_FILE"
+ prln_warning "\nATTENTION: No cipher mapping file found!"
+ outln "Please note from 2.9 on $PROG_NAME needs files in \"\$TESTSSL_INSTALL_DIR/etc/\" to function correctly."
+ outln
+ ignore_no_or_lame "Type \"yes\" to ignore this warning and proceed at your own risk" "yes"
+ [[ $? -ne 0 ]] && exit $ERR_RESOURCE
+ fi
+
+ TLS_DATA_FILE="$TESTSSL_INSTALL_DIR/etc/tls_data.txt"
+ if [[ ! -r "$TLS_DATA_FILE" ]]; then
+ prln_warning "\nATTENTION: No TLS data file found -- needed for socket-based handshakes"
+ outln "Please note from 2.9 on $PROG_NAME needs files in \"\$TESTSSL_INSTALL_DIR/etc/\" to function correctly."
+ outln
+ ignore_no_or_lame "Type \"yes\" to ignore this warning and proceed at your own risk" "yes"
+ [[ $? -ne 0 ]] && exit $ERR_RESOURCE
+ else
+ : # see #705, in a nutshell: not portable to initialize a global array inside a function. Thus it'll be done in main part below
+ fi
+}
+
+
+test_openssl_suffix() {
+ local naming_ext="$(uname).$(uname -m)"
+ local uname_arch="$(uname -m)"
+ local myarch_suffix=""
+
+ [[ $uname_arch =~ 64 ]] && myarch_suffix=64 || myarch_suffix=32
+ if [[ -f "$1/openssl" ]] && [[ -x "$1/openssl" ]]; then
+ OPENSSL="$1/openssl"
+ return 0
+ elif [[ -f "$1/openssl.$naming_ext" ]] && [[ -x "$1/openssl.$naming_ext" ]]; then
+ OPENSSL="$1/openssl.$naming_ext"
+ return 0
+ elif [[ -f "$1/openssl.$uname_arch" ]] && [[ -x "$1/openssl.$uname_arch" ]]; then
+ OPENSSL="$1/openssl.$uname_arch"
+ return 0
+ elif [[ -f "$1/openssl$myarch_suffix" ]] && [[ -x "$1/openssl$myarch_suffix" ]]; then
+ OPENSSL="$1/openssl$myarch_suffix"
+ return 0
+ fi
+ return 1
+}
+
+
+find_openssl_binary() {
+ local s_client_has=$TEMPDIR/s_client_has.txt
+ local s_client_starttls_has=$TEMPDIR/s_client_starttls_has.txt
+ local openssl_location cwd=""
+ local ossl_wo_dev_info
+ local curve
+ local -a curves_ossl=("sect163k1" "sect163r1" "sect163r2" "sect193r1" "sect193r2" "sect233k1" "sect233r1" "sect239k1" "sect283k1" "sect283r1" "sect409k1" "sect409r1" "sect571k1" "sect571r1" "secp160k1" "secp160r1" "secp160r2" "secp192k1" "prime192v1" "secp224k1" "secp224r1" "secp256k1" "prime256v1" "secp384r1" "secp521r1" "brainpoolP256r1" "brainpoolP384r1" "brainpoolP512r1" "X25519" "X448")
+
+ # 0. check environment variable whether it's executable
+ if [[ -n "$OPENSSL" ]] && [[ ! -x "$OPENSSL" ]]; then
+ prln_warning "\ncannot find specified (\$OPENSSL=$OPENSSL) binary."
+ tmln_out " Looking some place else ..."
+ elif [[ -x "$OPENSSL" ]]; then
+ : # 1. all ok supplied $OPENSSL was found and has executable bit set -- testrun comes below
+ elif [[ -e "/mnt/c/Windows/System32/bash.exe" ]] && test_openssl_suffix "$(dirname "$(type -p openssl)")"; then
+ # 2. otherwise, only if on Bash on Windows, use system binaries only.
+ SYSTEM2="WSL"
+ elif test_openssl_suffix "$TESTSSL_INSTALL_DIR"; then
+ : # 3. otherwise try openssl in path of testssl.sh
+ elif test_openssl_suffix "$TESTSSL_INSTALL_DIR/bin"; then
+ : # 4. otherwise here, this is supposed to be the standard --platform independent path in the future!!!
+ elif test_openssl_suffix "$(dirname "$(type -p openssl)")"; then
+ : # 5. we tried hard and failed, so now we use the system binaries
+ fi
+
+ [[ ! -x "$OPENSSL" ]] && fatal "cannot exec or find any openssl binary" $ERR_OSSLBIN
+
+ # The former detection only was flawed, because when the system supplied openssl.cnf file
+ # couldn't be parsed by our openssl it bailed out here with a misleading error, see #1982.
+ # Now we try with another version of the config file and if it still fails we bail out.
+ if ! $OPENSSL version -d >/dev/null 2>&1 ; then
+ export OPENSSL_CONF="$TESTSSL_INSTALL_DIR/etc/openssl.cnf"
+ if ! $OPENSSL version -d >/dev/null 2>&1 ; then
+ fatal "cannot exec or find any openssl binary" $ERR_OSSLBIN
+ else
+ [[ "$DEBUG" -ge 1 ]] && echo "We provide our own openssl.cnf file as the one from your system cannot be used"
+ fi
+ fi
+
+ # https://www.openssl.org/news/openssl-notes.html
+ OSSL_NAME=$($OPENSSL version 2>/dev/null | awk '{ print $1 }')
+ OSSL_VER=$($OPENSSL version 2>/dev/null | awk -F' ' '{ print $2 }')
+ OSSL_VER_MAJOR="${OSSL_VER%%\.*}"
+ ossl_wo_dev_info="${OSSL_VER%%-*}"
+ OSSL_VER_MINOR="${ossl_wo_dev_info#$OSSL_VER_MAJOR\.}"
+ OSSL_VER_MINOR="${OSSL_VER_MINOR%%[a-zA-Z]*}"
+ OSSL_VER_APPENDIX="${OSSL_VER#$OSSL_VER_MAJOR\.$OSSL_VER_MINOR}"
+ OSSL_VER_PLATFORM=$($OPENSSL version -p 2>/dev/null | sed 's/^platform: //')
+ OSSL_BUILD_DATE=$($OPENSSL version -a 2>/dev/null | grep '^built' | sed -e 's/built on//' -e 's/: ... //' -e 's/: //' -e 's/ UTC//' -e 's/ +0000//' -e 's/.000000000//')
+
+ # see #190, reverting logic: unless otherwise proved openssl has no dh bits
+ case "$OSSL_VER_MAJOR.$OSSL_VER_MINOR" in
+ 1.0.2|1.1.0|1.1.1|3.0.0) HAS_DH_BITS=true ;;
+ esac
+ if [[ "$OSSL_NAME" =~ LibreSSL ]]; then
+ [[ ${OSSL_VER//./} -ge 210 ]] && HAS_DH_BITS=true
+ if "$SSL_NATIVE"; then
+ outln
+ pr_warning "LibreSSL in native ssl mode is not a good choice for testing INSECURE features!"
+ fi
+ fi
+
+ initialize_engine
+
+ openssl_location="$(type -p $OPENSSL)"
+ [[ -n "$GIT_REL" ]] && \
+ cwd="$PWD" || \
+ cwd="$RUN_DIR"
+ if [[ "$openssl_location" == ${PWD}/bin ]]; then
+ OPENSSL_LOCATION="\$PWD/bin/$(basename "$openssl_location")"
+ elif [[ "$openssl_location" =~ $cwd ]] && [[ "$cwd" != '.' ]]; then
+ OPENSSL_LOCATION="${openssl_location%%$cwd}"
+ else
+ OPENSSL_LOCATION="$openssl_location"
+ fi
+
+ OSSL_CIPHERS_S=""
+ HAS_SSL2=false
+ HAS_SSL3=false
+ HAS_TLS13=false
+ HAS_X448=false
+ HAS_X25519=false
+ HAS_NO_SSL2=false
+ HAS_NOSERVERNAME=false
+ HAS_CIPHERSUITES=false
+ HAS_COMP=false
+ HAS_NO_COMP=false
+ HAS_CURVES=false
+ OSSL_SUPPORTED_CURVES=""
+ HAS_PKEY=false
+ HAS_PKUTIL=false
+ HAS_ALPN=false
+ HAS_NPN=false
+ HAS_FALLBACK_SCSV=false
+ HAS_PROXY=false
+ HAS_XMPP=false
+ HAS_POSTGRES=false
+ HAS_MYSQL=false
+ HAS_LMTP=false
+ HAS_NNTP=false
+ HAS_IRC=false
+ HAS_CHACHA20=false
+ HAS_AES128_GCM=false
+ HAS_AES256_GCM=false
+ HAS_ZLIB=false
+
+ $OPENSSL ciphers -s 2>&1 | grep -aiq "unknown option" || \
+ OSSL_CIPHERS_S="-s"
+
+ # This and all other occurrences we do a little trick using "invalid." to avoid plain and
+ # link level DNS lookups. See issue #1418 and https://tools.ietf.org/html/rfc6761#section-6.4
+ $OPENSSL s_client -ssl2 -connect invalid. 2>&1 | grep -aiq "unknown option" || \
+ HAS_SSL2=true
+
+ $OPENSSL s_client -ssl3 -connect invalid. 2>&1 | grep -aiq "unknown option" || \
+ HAS_SSL3=true
+
+ $OPENSSL s_client -tls1_3 -connect invalid. 2>&1 | grep -aiq "unknown option" || \
+ HAS_TLS13=true
+
+ $OPENSSL genpkey -algorithm X448 2>&1 | grep -aq "not found" || \
+ HAS_X448=true
+
+ $OPENSSL genpkey -algorithm X25519 2>&1 | grep -aq "not found" || \
+ HAS_X25519=true
+
+ $OPENSSL s_client -no_ssl2 -connect invalid. 2>&1 | grep -aiq "unknown option" || \
+ HAS_NO_SSL2=true
+
+ $OPENSSL s_client -noservername -connect invalid. 2>&1 | grep -aiq "unknown option" || \
+ HAS_NOSERVERNAME=true
+
+ $OPENSSL s_client -ciphersuites -connect invalid. 2>&1 | grep -aiq "unknown option" || \
+ HAS_CIPHERSUITES=true
+
+ $OPENSSL s_client -comp -connect invalid. 2>&1 | grep -aiq "unknown option" || \
+ HAS_COMP=true
+
+ $OPENSSL s_client -no_comp -connect invalid. 2>&1 | grep -aiq "unknown option" || \
+ HAS_NO_COMP=true
+
+ OPENSSL_NR_CIPHERS=$(count_ciphers "$(actually_supported_osslciphers 'ALL:COMPLEMENTOFALL' 'ALL')")
+
+ if $OPENSSL s_client -curves "${curves_ossl[0]}" -connect invalid. 2>&1 | grep -aiq "unknown option"; then
+ for curve in "${curves_ossl[@]}"; do
+ $OPENSSL s_client -groups $curve -connect invalid.:8443 2>&1 | grep -Eiaq "Error with command|unknown option|Failed to set groups"
+ [[ $? -ne 0 ]] && OSSL_SUPPORTED_CURVES+=" $curve "
+ done
+ else
+ HAS_CURVES=true
+ for curve in "${curves_ossl[@]}"; do
+ $OPENSSL s_client -curves $curve -connect invalid. 2>&1 | grep -Eiaq "Error with command|unknown option"
+ [[ $? -ne 0 ]] && OSSL_SUPPORTED_CURVES+=" $curve "
+ done
+ fi
+
+ $OPENSSL pkey -help 2>&1 | grep -q Error || \
+ HAS_PKEY=true
+
+ $OPENSSL pkeyutl 2>&1 | grep -q Error || \
+ HAS_PKUTIL=true
+
+ # For the following we feel safe enough to query the s_client help functions.
+ # That was not good enough for the previous lookups
+ $OPENSSL s_client -help 2>$s_client_has
+
+ $OPENSSL s_client -starttls foo 2>$s_client_starttls_has
+
+ grep -qw '\-alpn' $s_client_has && \
+ HAS_ALPN=true
+
+ grep -qw '\-nextprotoneg' $s_client_has && \
+ HAS_NPN=true
+
+ grep -qw '\-fallback_scsv' $s_client_has && \
+ HAS_FALLBACK_SCSV=true
+
+ grep -q '\-proxy' $s_client_has && \
+ HAS_PROXY=true
+
+ grep -q '\-xmpp' $s_client_has && \
+ HAS_XMPP=true
+
+ grep -q 'postgres' $s_client_starttls_has && \
+ HAS_POSTGRES=true
+
+ grep -q 'mysql' $s_client_starttls_has && \
+ HAS_MYSQL=true
+
+ grep -q 'lmtp' $s_client_starttls_has && \
+ HAS_LMTP=true
+
+ grep -q 'nntp' $s_client_starttls_has && \
+ HAS_NNTP=true
+
+ grep -q 'irc' $s_client_starttls_has && \
+ HAS_IRC=true
+
+ $OPENSSL enc -chacha20 -K 12345678901234567890123456789012 -iv 01000000123456789012345678901234 > /dev/null 2> /dev/null <<< "test"
+ [[ $? -eq 0 ]] && HAS_CHACHA20=true
+
+ $OPENSSL enc -aes-128-gcm -K 0123456789abcdef0123456789abcdef -iv 0123456789abcdef01234567 > /dev/null 2> /dev/null <<< "test"
+ [[ $? -eq 0 ]] && HAS_AES128_GCM=true
+
+ $OPENSSL enc -aes-256-gcm -K 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef -iv 0123456789abcdef01234567 > /dev/null 2> /dev/null <<< "test"
+ [[ $? -eq 0 ]] && HAS_AES256_GCM=true
+
+ [[ "$(echo -e "\x78\x9C\xAB\xCA\xC9\x4C\xE2\x02\x00\x06\x20\x01\xBC" | $OPENSSL zlib -d 2>/dev/null)" == zlib ]] && HAS_ZLIB=true
+
+ if [[ -n "$CONNECT_TIMEOUT" ]] || [[ -n "$OPENSSL_TIMEOUT" ]]; then
+ # We don't set a general timeout as we might not have "timeout" installed and we only
+ # do what is instructed. Thus we check first what the command line params were,
+ # then we proceed
+ if type -p timeout >/dev/null 2>&1; then
+ # There are different versions of "timeout". Check whether --preserve-status is supported
+ if timeout --help 2>/dev/null | grep -q 'preserve-status'; then
+ TIMEOUT_CMD="timeout --preserve-status"
+ else
+ TIMEOUT_CMD="timeout"
+ fi
+ else
+ TIMEOUT_CMD=""
+ outln
+ fatal "You specified a connect or openssl timeout but the binary \"timeout\" couldn't be found " $ERR_RESOURCE
+ fi
+ fi
+
+ if ! "$do_mass_testing"; then
+ if [[ -n $OPENSSL_TIMEOUT ]]; then
+ OPENSSL="$TIMEOUT_CMD $OPENSSL_TIMEOUT $OPENSSL"
+ fi
+ fi
+
+ return 0
+}
+
+
+check4openssl_oldfarts() {
+ case "$OSSL_VER" in
+ 0.9.7*|0.9.6*|0.9.5*)
+ # 0.9.5a was latest in 0.9.5 an released 2000/4/1, that'll NOT suffice for this test
+ old_fart ;;
+ 0.9.8)
+ case $OSSL_VER_APPENDIX in
+ a|b|c|d|e) old_fart;; # no SNI!
+ # other than that we leave this for MacOSX and FreeBSD but it's a pain and likely gives false negatives/positives
+ esac
+ ;;
+ esac
+ if [[ $OSSL_VER_MAJOR -lt 1 ]]; then ## mm: Patch for libressl
+ prln_warning " Your \"$OPENSSL\" is way too old (<version 1.0) !"
+ case $SYSTEM in
+ *BSD|Darwin)
+ out " Please use binary provided in \$INSTALLDIR/bin/ or from ports/brew or compile from "
+ pr_url "github.com/PeterMosmans/openssl"; outln "."
+ fileout_insert_warning "too_old_openssl" "WARN" "Your $OPENSSL $OSSL_VER version is way too old. Please use binary provided in \$INSTALLDIR/bin/ or from ports/brew or compile from github.com/PeterMosmans/openssl ." ;;
+ *) out " Update openssl binaries or compile from "
+ pr_url "https://github.com/PeterMosmans/openssl"; outln "."
+ fileout_insert_warning "too_old_openssl" "WARN" "Update openssl binaries or compile from https://github.com/PeterMosmans/openssl .";;
+ esac
+ ignore_no_or_lame " Type \"yes\" to accept false negatives or positives" "yes"
+ [[ $? -ne 0 ]] && exit $ERR_CLUELESS
+ fi
+ outln
+}
+
+# FreeBSD needs to have /dev/fd mounted. This is a friendly hint, see #258
+check_bsd_mount() {
+ if [[ "$(uname)" == FreeBSD ]]; then
+ if ! mount | grep -q "^devfs"; then
+ outln "you seem to run $PROG_NAME= in a jail. Hopefully you're did \"mount -t fdescfs fdesc /dev/fd\""
+ elif mount | grep '/dev/fd' | grep -q fdescfs; then
+ :
+ else
+ fatal "You need to mount fdescfs on FreeBSD: \"mount -t fdescfs fdesc /dev/fd\"" $ERR_OTHERCLIENT
+ fi
+ fi
+}
+
+# This sets the PRINTF command for writing into TCP sockets. It is needed because
+# The shell builtin printf flushes the write buffer at every \n, ("\x0a") which
+# in turn means a new TCP fragment. That causes a slight performance penalty and
+# and some F5s to hiccup, see #1113. Unfortunately this can be used only with GNU's
+# and OpenBSD's /usr/bin/printf -- FreeBSD + OS X can't do this. Thus here we need
+# to pipe through dd or cat, see socksend() and socksend_clienthello(). An empty
+# $PRINTF signals the bash internal printf which then uses cat as a stdout buffer.
+# A better solution needs to follow.
+#
+choose_printf() {
+ local p ptf
+
+ ptf="$(type -aP printf)"
+ if [[ -n "$ptf" ]]; then
+ for p in $ptf; do
+ if $p "\xc0\x14\xc0\xff\xee" | hexdump -C | grep -q 'c0 14 c0 ff ee'; then
+ PRINTF=$p
+ return 0
+ fi
+ done
+ fi
+ if type -t printf >/dev/null; then
+ PRINTF=""
+ return 0
+ fi
+ fatal "Neither external printf nor shell internal found. " $ERR_CLUELESS
+}
+
+
+help() {
+ cat << EOF
+
+ "$PROG_NAME [options] <URI>" or "$PROG_NAME <options>"
+
+
+"$PROG_NAME <options>", where <options> is:
+
+ --help what you're looking at
+ -b, --banner displays banner + version of $PROG_NAME
+ -v, --version same as previous
+ -V, --local pretty print all local ciphers
+ -V, --local <pattern> which local ciphers with <pattern> are available? If pattern is not a number: word match
+
+ <pattern> is always an ignore case word pattern of cipher hexcode or any other string in the name, kx or bits
+
+"$PROG_NAME <URI>", where <URI> is:
+
+ <URI> host|host:port|URL|URL:port port 443 is default, URL can only contain HTTPS protocol)
+
+"$PROG_NAME [options] <URI>", where [options] is:
+
+ -t, --starttls <protocol> Does a default run against a STARTTLS enabled <protocol,
+ protocol is <ftp|smtp|lmtp|pop3|imap|xmpp|telnet|ldap|nntp|postgres|mysql>
+ --xmpphost <to_domain> For STARTTLS enabled XMPP it supplies the XML stream to-'' domain -- sometimes needed
+ --mx <domain/host> Tests MX records from high to low priority (STARTTLS, port 25)
+ --file/-iL <fname> Mass testing option: Reads one testssl.sh command line per line from <fname>.
+ Can be combined with --serial or --parallel. Implicitly turns on "--warnings batch".
+ Text format 1: Comments via # allowed, EOF signals end of <fname>
+ Text format 2: nmap output in greppable format (-oG), 1 port per line allowed
+ --mode <serial|parallel> Mass testing to be done serial (default) or parallel (--parallel is shortcut for the latter)
+ --warnings <batch|off> "batch" doesn't continue when a testing error is encountered, off continues and skips warnings
+ --connect-timeout <seconds> useful to avoid hangers. Max <seconds> to wait for the TCP socket connect to return
+ --openssl-timeout <seconds> useful to avoid hangers. Max <seconds> to wait before openssl connect will be terminated
+
+single check as <options> ("$PROG_NAME URI" does everything except -E and -g):
+ -e, --each-cipher checks each local cipher remotely
+ -E, --cipher-per-proto checks those per protocol
+ -s, --std, --standard tests standard cipher categories by strength
+ -p, --protocols checks TLS/SSL protocols (including SPDY/HTTP2)
+ -g, --grease tests several server implementation bugs like GREASE and size limitations
+ -S, --server-defaults displays the server's default picks and certificate info
+ -P, --server-preference displays the server's picks: protocol+cipher
+ -x, --single-cipher <pattern> tests matched <pattern> of ciphers
+ (if <pattern> not a number: word match)
+ -c, --client-simulation test client simulations, see which client negotiates with cipher and protocol
+ -h, --header, --headers tests HSTS, HPKP, server/app banner, security headers, cookie, reverse proxy, IPv4 address
+
+ -U, --vulnerable tests all (of the following) vulnerabilities (if applicable)
+ -H, --heartbleed tests for Heartbleed vulnerability
+ -I, --ccs, --ccs-injection tests for CCS injection vulnerability
+ -T, --ticketbleed tests for Ticketbleed vulnerability in BigIP loadbalancers
+ -BB, --robot tests for Return of Bleichenbacher's Oracle Threat (ROBOT) vulnerability
+ -R, --renegotiation tests for renegotiation vulnerabilities
+ -C, --compression, --crime tests for CRIME vulnerability (TLS compression issue)
+ -B, --breach tests for BREACH vulnerability (HTTP compression issue)
+ -O, --poodle tests for POODLE (SSL) vulnerability
+ -Z, --tls-fallback checks TLS_FALLBACK_SCSV mitigation
+ -W, --sweet32 tests 64 bit block ciphers (3DES, RC2 and IDEA): SWEET32 vulnerability
+ -A, --beast tests for BEAST vulnerability
+ -L, --lucky13 tests for LUCKY13
+ -F, --freak tests for FREAK vulnerability
+ -J, --logjam tests for LOGJAM vulnerability
+ -D, --drown tests for DROWN vulnerability
+ -f, --pfs, --fs, --nsa checks (perfect) forward secrecy settings
+ -4, --rc4, --appelbaum which RC4 ciphers are being offered?
+
+tuning / connect options (most also can be preset via environment variables):
+ --fast omits some checks: using openssl for all ciphers (-e), show only first preferred cipher.
+ -9, --full includes tests for implementation bugs and cipher per protocol (could disappear)
+ --bugs enables the "-bugs" option of s_client, needed e.g. for some buggy F5s
+ --assume-http if protocol check fails it assumes HTTP protocol and enforces HTTP checks
+ --ssl-native fallback to checks with OpenSSL where sockets are normally used
+ --openssl <PATH> use this openssl binary (default: look in \$PATH, \$RUN_DIR of $PROG_NAME)
+ --proxy <host:port|auto> (experimental) proxy connects via <host:port>, auto: values from \$env (\$http(s)_proxy)
+ -6 also use IPv6. Works only with supporting OpenSSL version and IPv6 connectivity
+ --ip <ip> a) tests the supplied <ip> v4 or v6 address instead of resolving host(s) in URI
+ b) arg "one" means: just test the first DNS returns (useful for multiple IPs)
+ -n, --nodns <min|none> if "none": do not try any DNS lookups, "min" queries A, AAAA and MX records
+ --sneaky leave less traces in target logs: user agent, referer
+ --ids-friendly skips a few vulnerability checks which may cause IDSs to block the scanning IP
+ --phone-out allow to contact external servers for CRL download and querying OCSP responder
+ --add-ca <cafile> path to <cafile> or a comma separated list of CA files enables test against additional CAs.
+ --basicauth <user:pass> provide HTTP basic auth information.
+
+output options (can also be preset via environment variables):
+ --quiet don't output the banner. By doing this you acknowledge usage terms normally appearing in the banner
+ --wide wide output for tests like RC4, BEAST. PFS also with hexcode, kx, strength, RFC name
+ --show-each for wide outputs: display all ciphers tested -- not only succeeded ones
+ --mapping <openssl| openssl: use the OpenSSL cipher suite name as the primary name cipher suite name form (default)
+ iana|rfc -> use the IANA/(RFC) cipher suite name as the primary name cipher suite name form
+ no-openssl| -> don't display the OpenSSL cipher suite name, display IANA/(RFC) names only
+ no-iana|no-rfc> -> don't display the IANA/(RFC) cipher suite name, display OpenSSL names only
+ --color <0|1|2|3> 0: no escape or other codes, 1: b/w escape codes, 2: color (default), 3: extra color (color all ciphers)
+ --colorblind swap green and blue in the output
+ --debug <0-6> 1: screen output normal but keeps debug output in /tmp/. 2-6: see "grep -A 5 '^DEBUG=' testssl.sh"
+
+file output options (can also be preset via environment variables)
+ --log, --logging logs stdout to '\${NODE}-p\${port}\${YYYYMMDD-HHMM}.log' in current working directory (cwd)
+ --logfile|-oL <logfile> logs stdout to 'dir/\${NODE}-p\${port}\${YYYYMMDD-HHMM}.log'. If 'logfile' is a dir or to a specified 'logfile'
+ --json additional output of findings to flat JSON file '\${NODE}-p\${port}\${YYYYMMDD-HHMM}.json' in cwd
+ --jsonfile|-oj <jsonfile> additional output to the specified flat JSON file or directory, similar to --logfile
+ --json-pretty additional JSON structured output of findings to a file '\${NODE}-p\${port}\${YYYYMMDD-HHMM}.json' in cwd
+ --jsonfile-pretty|-oJ <jsonfile> additional JSON structured output to the specified file or directory, similar to --logfile
+ --csv additional output of findings to CSV file '\${NODE}-p\${port}\${YYYYMMDD-HHMM}.csv' in cwd or directory
+ --csvfile|-oC <csvfile> additional output as CSV to the specified file or directory, similar to --logfile
+ --html additional output as HTML to file '\${NODE}-p\${port}\${YYYYMMDD-HHMM}.html'
+ --htmlfile|-oH <htmlfile> additional output as HTML to the specified file or directory, similar to --logfile
+ --out(f,F)ile|-oa/-oA <fname> log to a LOG,JSON,CSV,HTML file (see nmap). -oA/-oa: pretty/flat JSON.
+ "auto" uses '\${NODE}-p\${port}\${YYYYMMDD-HHMM}'. If fname if a dir uses 'dir/\${NODE}-p\${port}\${YYYYMMDD-HHMM}'
+ --hints additional hints to findings
+ --severity <severity> severities with lower level will be filtered for CSV+JSON, possible values <LOW|MEDIUM|HIGH|CRITICAL>
+ --append if (non-empty) <logfile>, <csvfile>, <jsonfile> or <htmlfile> exists, append to file. Omits any header
+ --outprefix <fname_prefix> before '\${NODE}.' above prepend <fname_prefix>
+
+
+Options requiring a value can also be called with '=' e.g. testssl.sh -t=smtp --wide --openssl=/usr/bin/openssl <URI>.
+<URI> always needs to be the last parameter.
+
+EOF
+ # Set HTMLHEADER and JSONHEADER to false so that the cleanup() function won't
+ # try to write footers to the HTML and JSON files.
+ HTMLHEADER=false
+ JSONHEADER=false
+ #' Fix syntax highlight on sublime
+ "$CHILD_MASS_TESTING" && kill -s USR1 $PPID
+ exit $1
+}
+
+maketempf() {
+ TEMPDIR=$(mktemp -d /tmp/testssl.XXXXXX)
+ if [[ $? -ne 0 ]]; then
+ # For e.g. devices where we can't write to /tmp we chose $PWD but we can't
+ # allow every char as we haven't quoted all strings depending on it, see #1445
+ if [[ $PWD =~ [^A-Za-z0-9\.,/_-] ]]; then
+ fatal "\$PWD contains illegal chars: \"$BASH_REMATCH\"" $ERR_FCREATE
+ fi
+ TEMPDIR=$(mktemp -d "$PWD/testssl.XXXXXX") || exit $ERR_FCREATE
+ fi
+ TMPFILE=$TEMPDIR/tempfile.txt || exit $ERR_FCREATE
+ if [[ "$DEBUG" -eq 0 ]]; then
+ ERRFILE="/dev/null"
+ else
+ ERRFILE=$TEMPDIR/errorfile.txt || exit $ERR_FCREATE
+ fi
+ HOSTCERT=$TEMPDIR/host_certificate.pem
+}
+
+prepare_debug() {
+ if [[ $DEBUG -ne 0 ]]; then
+ cat >$TEMPDIR/environment.txt << EOF
+
+
+GIT_REL: $GIT_REL
+
+PID: $$
+commandline: "${CMDLINE[@]}"
+bash version: ${BASH_VERSINFO[0]}.${BASH_VERSINFO[1]}.${BASH_VERSINFO[2]}
+status: ${BASH_VERSINFO[4]}
+machine: ${BASH_VERSINFO[5]}
+operating system: $SYSTEM $SYSTEMREV
+os constraint: $SYSTEM2
+shellopts: $SHELLOPTS
+printf: $PRINTF
+NO_ITALICS: $NO_ITALICS
+
+$($OPENSSL version -a 2>/dev/null)
+OSSL_VER_MAJOR: $OSSL_VER_MAJOR
+OSSL_VER_MINOR: $OSSL_VER_MINOR
+OSSL_VER_APPENDIX: $OSSL_VER_APPENDIX
+OSSL_BUILD_DATE: $OSSL_BUILD_DATE
+OSSL_VER_PLATFORM: $OSSL_VER_PLATFORM
+
+OPENSSL_NR_CIPHERS: $OPENSSL_NR_CIPHERS
+OPENSSL_CONF: $OPENSSL_CONF
+HAS_CURVES: $HAS_CURVES
+OSSL_SUPPORTED_CURVES: $OSSL_SUPPORTED_CURVES
+
+HAS_IPv6: $HAS_IPv6
+HAS_SSL2: $HAS_SSL2
+HAS_SSL3: $HAS_SSL3
+HAS_TLS13: $HAS_TLS13
+HAS_X448: $HAS_X448
+HAS_X25519: $HAS_X25519
+HAS_NO_SSL2: $HAS_NO_SSL2
+HAS_SPDY: $HAS_SPDY
+HAS_ALPN: $HAS_ALPN
+HAS_FALLBACK_SCSV: $HAS_FALLBACK_SCSV
+HAS_COMP: $HAS_COMP
+HAS_NO_COMP: $HAS_NO_COMP
+HAS_CIPHERSUITES: $HAS_CIPHERSUITES
+HAS_PKEY: $HAS_PKEY
+HAS_PKUTIL: $HAS_PKUTIL
+HAS_PROXY: $HAS_PROXY
+HAS_XMPP: $HAS_XMPP
+HAS_POSTGRES: $HAS_POSTGRES
+HAS_MYSQL: $HAS_MYSQL
+HAS_LMTP: $HAS_LMTP
+HAS_NNTP: $HAS_NNTP
+HAS_IRC: $HAS_IRC
+
+HAS_DIG: $HAS_DIG
+HAS_HOST: $HAS_HOST
+HAS_DRILL: $HAS_DRILL
+HAS_NSLOOKUP: $HAS_NSLOOKUP
+HAS_IDN: $HAS_IDN
+HAS_IDN2: $HAS_IDN2
+HAS_AVAHIRESOLVE: $HAS_AVAHIRESOLVE
+HAS_DIG_NOIDNOUT: $HAS_DIG_NOIDNOUT
+HAS_DIG_R: $HAS_DIG_R
+
+PATH: $PATH
+PROG_NAME: $PROG_NAME
+TESTSSL_INSTALL_DIR: $TESTSSL_INSTALL_DIR
+RUN_DIR: $RUN_DIR
+CIPHERS_BY_STRENGTH_FILE: $CIPHERS_BY_STRENGTH_FILE
+
+CAPATH: $CAPATH
+COLOR: $COLOR
+COLORBLIND: $COLORBLIND
+TERM_WIDTH: $TERM_WIDTH
+INTERACTIVE: $INTERACTIVE
+HAS_GNUDATE: $HAS_GNUDATE
+HAS_FREEBSDDATE: $HAS_FREEBSDDATE
+HAS_OPENBSDDATE: $HAS_OPENBSDDATE
+HAS_SED_E: $HAS_SED_E
+
+SHOW_EACH_C: $SHOW_EACH_C
+SSL_NATIVE: $SSL_NATIVE
+ASSUME_HTTP $ASSUME_HTTP
+BASICAUTH: $BASICAUTH
+SNEAKY: $SNEAKY
+OFFENSIVE: $OFFENSIVE
+PHONE_OUT: $PHONE_OUT
+
+DEBUG: $DEBUG
+
+HSTS_MIN: $HSTS_MIN
+HPKP_MIN: $HPKP_MIN
+CLIENT_MIN_PFS: $CLIENT_MIN_PFS
+DAYS2WARN1: $DAYS2WARN1
+DAYS2WARN2: $DAYS2WARN2
+
+HEADER_MAXSLEEP: $HEADER_MAXSLEEP
+MAX_WAITSOCK: $MAX_WAITSOCK
+HEARTBLEED_MAX_WAITSOCK: $HEARTBLEED_MAX_WAITSOCK
+CCS_MAX_WAITSOCK: $CCS_MAX_WAITSOCK
+USLEEP_SND $USLEEP_SND
+USLEEP_REC $USLEEP_REC
+
+EOF
+ type -p locale &>/dev/null && locale >>$TEMPDIR/environment.txt || echo "locale doesn't exist" >>$TEMPDIR/environment.txt
+ actually_supported_osslciphers 'ALL:COMPLEMENTOFALL' 'ALL' "-V" &>$TEMPDIR/all_local_ciphers.txt
+ fi
+ # see also $TEMPDIR/s_client_has.txt from find_openssl_binary
+}
+
+
+prepare_arrays() {
+ local hexc mac ossl_ciph
+ local ossl_supported_tls="" ossl_supported_sslv2=""
+ local -i i=0
+
+ if [[ -e "$CIPHERS_BY_STRENGTH_FILE" ]]; then
+ "$HAS_SSL2" && ossl_supported_sslv2="$($OPENSSL ciphers -ssl2 -V 'ALL:COMPLEMENTOFALL:@STRENGTH' 2>$ERRFILE)"
+ if "$HAS_SSL2"; then
+ ossl_supported_tls="$(actually_supported_osslciphers 'ALL:COMPLEMENTOFALL:@STRENGTH' 'ALL' "-tls1 -V")"
+ else
+ ossl_supported_tls="$(actually_supported_osslciphers 'ALL:COMPLEMENTOFALL:@STRENGTH' 'ALL' "-V")"
+ fi
+ TLS13_OSSL_CIPHERS=""
+ while read hexc n TLS_CIPHER_OSSL_NAME[i] TLS_CIPHER_RFC_NAME[i] TLS_CIPHER_SSLVERS[i] TLS_CIPHER_KX[i] TLS_CIPHER_AUTH[i] TLS_CIPHER_ENC[i] mac TLS_CIPHER_EXPORT[i]; do
+ TLS_CIPHER_HEXCODE[i]="$hexc"
+ TLS_CIPHER_OSSL_SUPPORTED[i]=false
+ if [[ ${#hexc} -eq 9 ]]; then
+ # >= SSLv3 ciphers
+ if [[ $OSSL_VER_MAJOR -lt 1 ]]; then
+ [[ ":${ossl_supported_tls}:" =~ ":${TLS_CIPHER_OSSL_NAME[i]}:" ]] && TLS_CIPHER_OSSL_SUPPORTED[i]=true
+ else
+ ossl_ciph="$(awk '/'"$hexc"'/ { print $3 }' <<< "$ossl_supported_tls")"
+ if [[ -n "$ossl_ciph" ]]; then
+ TLS_CIPHER_OSSL_SUPPORTED[i]=true
+ [[ "$ossl_ciph" != ${TLS_CIPHER_OSSL_NAME[i]} ]] && TLS_CIPHER_OSSL_NAME[i]="$ossl_ciph"
+ [[ "${hexc:2:2}" == 13 ]] && TLS13_OSSL_CIPHERS+=":$ossl_ciph"
+ fi
+ fi
+ elif [[ $OSSL_VER_MAJOR -lt 1 ]]; then
+ [[ ":${ossl_supported_sslv2}:" =~ ":${TLS_CIPHER_OSSL_NAME[i]}:" ]] && TLS_CIPHER_OSSL_SUPPORTED[i]=true
+ else
+ [[ "$ossl_supported_sslv2" =~ $hexc ]] && TLS_CIPHER_OSSL_SUPPORTED[i]=true
+ fi
+ i+=1
+ done < "$CIPHERS_BY_STRENGTH_FILE"
+ fi
+ TLS_NR_CIPHERS=i
+ TLS13_OSSL_CIPHERS="${TLS13_OSSL_CIPHERS:1}"
+}
+
+
+mybanner() {
+ local idtag
+ local bb1 bb2 bb3
+
+ "$QUIET" && return
+ "$CHILD_MASS_TESTING" && return
+ OPENSSL_NR_CIPHERS=$(count_ciphers "$(actually_supported_osslciphers 'ALL:COMPLEMENTOFALL:@STRENGTH' 'ALL')")
+ [[ -n "$GIT_REL" ]] && idtag="$GIT_REL"
+ bb1=$(cat <<EOF
+
+###########################################################
+ $PROG_NAME $VERSION from
+EOF
+)
+ bb2=$(cat <<EOF
+
+ This program is free software. Distribution and
+ modification under GPLv2 permitted.
+ USAGE w/o ANY WARRANTY. USE IT AT YOUR OWN RISK!
+
+ Please file bugs @
+EOF
+)
+ bb3=$(cat <<EOF
+
+###########################################################
+EOF
+)
+ pr_bold "$bb1 "
+ pr_boldurl "$SWURL"; outln
+ if [[ -n "$idtag" ]]; then
+ #FIXME: if we run it not off the git dir we miss the version tag.
+ # at least we don't want to display empty brackets here...
+ pr_bold " ("
+ pr_grey "$idtag"
+ prln_bold ")"
+ fi
+ pr_bold "$bb2 "
+ pr_boldurl "https://testssl.sh/bugs/"; outln
+ pr_bold "$bb3"
+ outln "\n"
+ outln " Using \"$($OPENSSL version 2>/dev/null)\" [~$OPENSSL_NR_CIPHERS ciphers]"
+ out " on $HNAME:"
+ outln "$OPENSSL_LOCATION"
+ outln " (built: \"$OSSL_BUILD_DATE\", platform: \"$OSSL_VER_PLATFORM\")\n"
+}
+
+calc_scantime() {
+ END_TIME=$(date +%s)
+ SCAN_TIME=$(( END_TIME - START_TIME ))
+}
+
+cleanup() {
+ # If parallel mass testing is being performed, then the child tests need
+ # to be killed before $TEMPDIR is deleted. Otherwise, error messages
+ # will be created if testssl.sh is stopped before all testing is complete.
+ "$INTERACTIVE" && [[ $NR_PARALLEL_TESTS -gt 0 ]] && echo -en "\r \r" 1>&2
+ while [[ $NEXT_PARALLEL_TEST_TO_FINISH -lt $NR_PARALLEL_TESTS ]]; do
+ if [[ ${PARALLEL_TESTING_PID[NEXT_PARALLEL_TEST_TO_FINISH]} -ne 0 ]] && \
+ ps ${PARALLEL_TESTING_PID[NEXT_PARALLEL_TEST_TO_FINISH]} >/dev/null ; then
+ kill ${PARALLEL_TESTING_PID[NEXT_PARALLEL_TEST_TO_FINISH]} >&2 2>/dev/null
+ wait ${PARALLEL_TESTING_PID[NEXT_PARALLEL_TEST_TO_FINISH]} 2>/dev/null # make sure pid terminated, see wait(1p)
+ get_next_message_testing_parallel_result "stopped"
+ else
+ # If a test had already completed, but its output wasn't yet processed,
+ # then process it now.
+ get_next_message_testing_parallel_result "completed"
+ fi
+ NEXT_PARALLEL_TEST_TO_FINISH+=1
+ done
+ if [[ "$DEBUG" -ge 1 ]]; then
+ tmln_out
+ tm_underline "DEBUG (level $DEBUG): see files in $TEMPDIR"
+ tmln_out
+ else
+ [[ -d "$TEMPDIR" ]] && rm -rf "$TEMPDIR";
+ fi
+ outln
+ # No shorthand expression to avoid errors when $CMDLINE_PARSED haven't been filled yet.
+ if [[ $CMDLINE_PARSED == true ]]; then
+ "$SECTION_FOOTER_NEEDED" && fileout_section_footer true
+ html_footer
+ fileout_footer
+ fi
+ # debugging off, see above
+ grep -q xtrace <<< "$SHELLOPTS" && ! "$DEBUG_ALLINONE" && exec 2>&42 42>&-
+}
+
+child_error() {
+ cleanup
+ exit $ERR_CHILD
+}
+
+
+# Program terminates prematurely, with error code
+# arg1: string to print / to write to file
+# arg2: global error code, see ERR_* above
+# arg3: an optional hint (string)
+#
+fatal() {
+ outln
+ prln_magenta "Fatal error: $1" >&2
+ [[ -n "$LOGFILE" ]] && prln_magenta "Fatal error: $1" >>$LOGFILE
+ if [[ -n "$3" ]]; then
+ outln "$3" >&2
+ [[ -n "$LOGFILE" ]] && outln "$3" >>$LOGFILE
+ fi
+ # Make sure we don't try to write into files when not created yet.
+ # No shorthand expression to avoid errors when $CMDLINE_PARSED haven't been filled yet.
+ [[ $CMDLINE_PARSED == true ]] && fileout "scanProblem" "FATAL" "$1"
+ exit $2
+}
+
+# This OTOH doesn't exit but puts a fatal error to the screen but continues with the next
+# IP/hostname. It should only be used if a single IP/Hostname in a scan is not reachable.
+# arg1: string to print / to write to file
+#
+ip_fatal() {
+ outln
+ prln_magenta "Fatal error: $1, proceeding with next IP (if any)" >&2
+ [[ -n "$LOGFILE" ]] && prln_magenta "Fatal error: $1, proceeding with next IP (if any)" >>$LOGFILE
+ outln
+ fileout "scanProblem" "FATAL" "$1, proceeding with next IP (if any)"
+ return 0
+}
+
+# This generic function outputs an error onto the screen and handles logging.
+# arg1: string to print / to write to file, arg2 (optional): additional hint to write
+#
+generic_nonfatal() {
+ prln_magenta "$1" >&2
+ [[ -n $2 ]] && outln "$2"
+ [[ -n "$LOGFILE" ]] && prln_magenta "$1" >>$LOGFILE && [[ -n $2 ]] && outln "$2" >>$LOGFILE
+ outln
+ fileout "scanProblem" "WARN" "$1"
+ return 0
+}
+
+initialize_engine(){
+ # for now only GOST engine
+ grep -q '^# testssl config file' "$OPENSSL_CONF" 2>/dev/null && \
+ return 0 # We have been here already
+ if "$NO_ENGINE"; then
+ # Avoid potential conflicts also -- manual hook, see #1117
+ export OPENSSL_CONF=''
+ return 1
+ elif $OPENSSL engine gost -v 2>&1 | grep -Eq 'invalid command|no such engine'; then
+ outln
+ pr_warning "No engine or GOST support via engine with your $OPENSSL"; outln
+ fileout_insert_warning "engine_problem" "WARN" "No engine or GOST support via engine with your $OPENSSL"
+ export OPENSSL_CONF=''
+ return 1
+ elif ! $OPENSSL engine gost -vvvv -t -c 2>/dev/null >/dev/null; then
+ # check for openssl 1.1.1 config -- not this may not be reliable. We only use this
+ # to suppress the warning (confuses users), see #1119
+ # https://github.com/openssl/openssl/commit/b524b808a1d1ba204dbdcbb42de4e3bddb3472ac
+ if ! grep -q 'using the .include directive' /etc/ssl/openssl.cnf; then
+ outln
+ pr_warning "No engine or GOST support via engine with your $OPENSSL"; outln
+ fi
+ fileout_insert_warning "engine_problem" "WARN" "No engine or GOST support via engine with your $OPENSSL"
+ # Avoid clashes of OpenSSL 1.1.1 config file with our openssl 1.0.2. This is for Debian 10
+ export OPENSSL_CONF=''
+ return 1
+ else
+ # we have engine support. But we want to check whether an external OPENSSL_CONF was supplied.
+ # $TESTSSL_INSTALL_DIR/etc/openssl.cnf is an internal presetting, see #1982
+ if [[ -n "$OPENSSL_CONF" ]] && [[ "$OPENSSL_CONF" != "$TESTSSL_INSTALL_DIR/etc/openssl.cnf" ]]; then
+ prln_warning "For now I am providing the config file to have GOST support"
+ else
+ OPENSSL_CONF=$TEMPDIR/gost.conf
+ # see https://www.mail-archive.com/openssl-users@openssl.org/msg65395.html
+ cat >$OPENSSL_CONF << EOF
+# testssl config file for openssl
+
+openssl_conf = openssl_def
+
+[ openssl_def ]
+engines = engine_section
+
+[ engine_section ]
+gost = gost_section
+
+[ gost_section ]
+engine_id = gost
+default_algorithms = ALL
+CRYPT_PARAMS = id-Gost28147-89-CryptoPro-A-ParamSet
+
+EOF
+ [[ $? -ne 0 ]] && exit $ERR_OSSLBIN
+ export OPENSSL_CONF
+ fi
+ fi
+ return 0
+}
+
+# arg1: text to display before "-->"
+# arg2: arg needed to accept to continue
+ignore_no_or_lame() {
+ local a
+
+ [[ "$WARNINGS" == off ]] && return 0
+ [[ "$WARNINGS" == batch ]] && return 1
+ tm_warning "$1 --> "
+ read a
+ if [[ "$2" == "$(toupper "$2")" ]]; then
+ # all uppercase requested
+ if [[ "$a" == "$2" ]]; then
+ return 0
+ else
+ return 1
+ fi
+ elif [[ "$2" == "$(tolower "$a")" ]]; then
+ # we normalize the word to continue
+ return 0
+ else
+ return 1
+ fi
+}
+
+# arg1: URI
+parse_hn_port() {
+ local tmp_port
+ local node_tmp=""
+
+ NODE="$1"
+ NODE="${NODE/https\:\/\//}" # strip "https"
+ NODE="${NODE%%/*}" # strip trailing urlpath
+ if grep -q ':$' <<< "$NODE"; then
+ if grep -wq http <<< "$NODE"; then
+ fatal "\"http\" is not what you meant probably" $ERR_CMDLINE
+ else
+ fatal "\"$1\" is not a valid URI" $ERR_CMDLINE
+ fi
+ fi
+ # Was an IPv6 address supplied like [AA:BB:CC::]:port ?
+ if grep -q ']' <<< "$NODE"; then
+ tmp_port=$(printf "$NODE" | sed 's/\[.*\]//' | sed 's/://')
+ # determine v6 port, supposed it was supplied additionally
+ if [[ -n "$tmp_port" ]]; then
+ PORT=$tmp_port
+ NODE=$(sed "s/:$PORT//" <<< "$NODE")
+ fi
+ NODE=$(sed -e 's/\[//' -e 's/\]//' <<< "$NODE")
+ else
+ # determine v4 port, supposed it was supplied additionally
+ grep -q ':' <<< "$NODE" && \
+ PORT=$(sed 's/^.*\://' <<< "$NODE") && NODE=$(sed 's/\:.*$//' <<< "$NODE")
+ fi
+ NODE="${NODE%%.}" # strip trailing "." if supplied
+
+ # We check for non-ASCII chars now. If there are some we'll try to convert it if IDN/IDN2 is installed
+ # If not, we'll continue. Hoping later that dig can use it. If not the error handler will tell
+ # Honestly we don't care whether it's IDN2008 or IDN2003 or Emoji domains as long as it works.
+ # So we try to resolve anything supplied. If it can't our resolver error handler takes care
+ if [[ "$NODE" == *[![:ascii:]]* ]]; then
+ if ! "$HAS_IDN2" && ! "$HAS_IDN"; then
+ prln_warning " URI contains non-ASCII characters and libidn/libidn2 not available."
+ outln " Trying to feed the resolver without converted \"$NODE\" ...\n"
+ #ToDo: fileout is missing
+ node_tmp="$NODE"
+ elif "$HAS_IDN2"; then
+ node_tmp="$(idn2 "$NODE" 2>/dev/null)"
+ fi
+ if "$HAS_IDN" && [[ -z "$node_tmp" ]]; then
+ node_tmp="$(idn "$NODE" 2>/dev/null)"
+ fi
+ if [[ -z "$node_tmp" ]]; then
+ prln_warning " URI contains non-ASCII characters and IDN conversion failed."
+ outln " Trying to feed the resolver without converted \"$NODE\" ...\n"
+ #ToDo: fileout is missing
+ node_tmp="$NODE"
+ fi
+ NODE="$node_tmp"
+ fi
+
+ debugme echo $NODE:$PORT
+ SNI="-servername $NODE"
+ URL_PATH=$(sed 's/https:\/\///' <<< "$1" | sed 's/'"${NODE}"'//' | sed 's/.*'"${PORT}"'//') # remove protocol and node part and port
+ URL_PATH=$(sed 's/\/\//\//g' <<< "$URL_PATH") # we rather want // -> /
+ URL_PATH=${URL_PATH%%.} # strip trailing "." so that it is not interpreted as URL
+ [[ -z "$URL_PATH" ]] && URL_PATH="/"
+ debugme echo "URL_PATH: $URL_PATH"
+ return 0 # NODE, URL_PATH, PORT is set now
+}
+
+
+# args: string containing ip addresses
+filter_ip6_address() {
+ local a
+
+ for a in "$@"; do
+ if ! is_ipv6addr "$a"; then
+ continue
+ fi
+ if "$HAS_SED_E"; then
+ sed -E 's/^abcdeABCDEFf0123456789:]//g' <<< "$a" | sed -e '/^$/d' -e '/^;;/d'
+ else
+ sed -r 's/[^abcdefABCDEF0123456789:]//g' <<< "$a" | sed -e '/^$/d' -e '/^;;/d'
+ fi
+ done
+}
+
+filter_ip4_address() {
+ local a
+
+ for a in "$@"; do
+ if ! is_ipv4addr "$a"; then
+ continue
+ fi
+ if "$HAS_SED_E"; then
+ sed -E 's/[^[:digit:].]//g' <<< "$a" | sed -e '/^$/d'
+ else
+ sed -r 's/[^[:digit:].]//g' <<< "$a" | sed -e '/^$/d'
+ fi
+ done
+}
+
+# For security testing sometimes we have local entries. Getent is BS under Linux for localhost: No network, no resolution
+# arg1 is the entry we want to look up in the host file
+get_local_aaaa() {
+ local ip6=""
+ local etchosts="/etc/hosts /c/Windows/System32/drivers/etc/hosts"
+
+ [[ -z "$1" ]] && echo "" && return 1
+ # Also multiple records should work fine
+ ip6=$(grep -wih "$1" $etchosts 2>/dev/null | grep ':' | grep -Ev '^#|\.local' | grep -Ei "[[:space:]]$1" | awk '{ print $1 }')
+ if is_ipv6addr "$ip6"; then
+ echo "$ip6"
+ else
+ echo ""
+ fi
+}
+get_local_a() {
+ local ip4=""
+ local etchosts="/etc/hosts /c/Windows/System32/drivers/etc/hosts"
+
+ ip4=$(grep -wih "$1" $etchosts 2>/dev/null | grep -Ev ':|^#|\.local' | grep -Ei "[[:space:]]$1" | awk '{ print $1 }')
+ if is_ipv4addr "$ip4"; then
+ echo "$ip4"
+ else
+ echo ""
+ fi
+}
+
+# Does a hard exit if no lookup binary is provided
+# Checks for IDN capabilities also
+#
+check_resolver_bins() {
+ local saved_openssl_conf="$OPENSSL_CONF"
+
+ OPENSSL_CONF="" # see https://github.com/drwetter/testssl.sh/issues/134
+ type -p dig &> /dev/null && HAS_DIG=true
+ type -p host &> /dev/null && HAS_HOST=true
+ type -p drill &> /dev/null && HAS_DRILL=true
+ type -p nslookup &> /dev/null && HAS_NSLOOKUP=true
+ type -p avahi-resolve &>/dev/null && HAS_AVAHIRESOLVE=true
+ type -p idn &>/dev/null && HAS_IDN=true
+ type -p idn2 &>/dev/null && HAS_IDN2=true
+
+ if ! "$HAS_DIG" && ! "$HAS_HOST" && ! "$HAS_DRILL" && ! "$HAS_NSLOOKUP"; then
+ fatal "Neither \"dig\", \"host\", \"drill\" or \"nslookup\" is present" $ERR_DNSBIN
+ fi
+ if "$HAS_DIG"; then
+ # Old dig versions don't have an option to ignore $HOME/.digrc
+ if ! dig -h | grep -qE '\-r.*~/.digrc'; then
+ HAS_DIG_R=false
+ DIG_R=""
+ fi
+ if dig -h | grep -Eq idnout; then
+ HAS_DIG_NOIDNOUT=true
+ fi
+ fi
+ OPENSSL_CONF="$saved_openssl_conf" # see https://github.com/drwetter/testssl.sh/issues/134
+ return 0
+}
+
+# arg1: a host name. Returned will be 0-n IPv4 addresses
+# watch out: $1 can also be a cname! --> all checked
+get_a_record() {
+ local ip4=""
+ local saved_openssl_conf="$OPENSSL_CONF"
+ local noidnout=""
+
+ "$HAS_DIG_NOIDNOUT" && noidnout="+noidnout"
+ [[ "$NODNS" == none ]] && return 0 # if no DNS lookup was instructed, leave here
+ if [[ "$1" == localhost ]]; then
+ # This is a bit ugly but prevents from doing DNS lookups which could fail
+ echo 127.0.0.1
+ return 0
+ fi
+ if is_ipv4addr "$1"; then
+ # This saves walking through this. Also it avoids hangs e.g. if you run docker locally without reachabale DNS
+ echo $1
+ return 0
+ fi
+ OPENSSL_CONF="" # see https://github.com/drwetter/testssl.sh/issues/134
+ if [[ "$NODE" == *.local ]]; then
+ if "$HAS_AVAHIRESOLVE"; then
+ ip4=$(filter_ip4_address $(avahi-resolve -4 -n "$1" 2>/dev/null | awk '{ print $2 }'))
+ elif "$HAS_DIG"; then
+ ip4=$(filter_ip4_address $(dig $DIG_R @224.0.0.251 -p 5353 +short -t a +notcp "$1" 2>/dev/null | sed '/^;;/d'))
+ else
+ fatal "Local hostname given but no 'avahi-resolve' or 'dig' available." $ERR_DNSBIN
+ fi
+ fi
+ if [[ -z "$ip4" ]] && "$HAS_HOST"; then
+ ip4=$(filter_ip4_address $(host -t a "$1" 2>/dev/null | awk '/address/ { print $NF }'))
+ fi
+ if [[ -z "$ip4" ]] && "$HAS_DRILL"; then
+ ip4=$(filter_ip4_address $(drill a "$1" | awk '/ANSWER SECTION/,/AUTHORITY SECTION/ { print $NF }' | awk '/^[0-9]/'))
+ fi
+ if [[ -z "$ip4" ]] && "$HAS_DIG"; then
+ ip4=$(filter_ip4_address $(dig $DIG_R +short +timeout=2 +tries=2 $noidnout -t a "$1" 2>/dev/null | awk '/^[0-9]/ { print $1 }'))
+ fi
+ if [[ -z "$ip4" ]] && "$HAS_NSLOOKUP"; then
+ ip4=$(filter_ip4_address $(strip_lf "$(nslookup -querytype=a "$1" 2>/dev/null | awk '/^Name/ { getline; print $NF }')"))
+ fi
+ OPENSSL_CONF="$saved_openssl_conf" # see https://github.com/drwetter/testssl.sh/issues/134
+ safe_echo "$ip4"
+}
+
+# arg1: a host name. Returned will be 0-n IPv6 addresses
+# watch out: $1 can also be a cname! --> all checked
+get_aaaa_record() {
+ local ip6=""
+ local saved_openssl_conf="$OPENSSL_CONF"
+ local noidnout=""
+
+ "$HAS_DIG_NOIDNOUT" && noidnout="+noidnout"
+ [[ "$NODNS" == none ]] && return 0 # if no DNS lookup was instructed, leave here
+ OPENSSL_CONF="" # see https://github.com/drwetter/testssl.sh/issues/134
+ if is_ipv6addr "$1"; then
+ # This saves walking through this. Also it avoids hangs e.g. if you run docker locally without reachabale DNS
+ echo "$1"
+ return 0
+ elif is_ipv4addr "$1"; then
+ # we need also this here as get_aaaa_record is always called after get_a_record and we want to handle this at a low level
+ return 0
+ fi
+ if [[ -z "$ip6" ]]; then
+ if [[ "$NODE" == *.local ]]; then
+ if "$HAS_AVAHIRESOLVE"; then
+ ip6=$(filter_ip6_address $(avahi-resolve -6 -n "$1" 2>/dev/null | awk '{ print $2 }'))
+ elif "$HAS_DIG"; then
+ ip6=$(filter_ip6_address $(dig $DIG_R @ff02::fb -p 5353 -t aaaa +short +notcp "$NODE"))
+ else
+ fatal "Local hostname given but no 'avahi-resolve' or 'dig' available." $ERR_DNSBIN
+ fi
+ elif "$HAS_HOST"; then
+ ip6=$(filter_ip6_address $(host -t aaaa "$1" | awk '/address/ { print $NF }'))
+ elif "$HAS_DRILL"; then
+ ip6=$(filter_ip6_address $(drill aaaa "$1" | awk '/ANSWER SECTION/,/AUTHORITY SECTION/ { print $NF }' | awk '/^[0-9]/'))
+ elif "$HAS_DIG"; then
+ ip6=$(filter_ip6_address $(dig $DIG_R +short +timeout=2 +tries=2 $noidnout -t aaaa "$1" 2>/dev/null | awk '/^[0-9]/ { print $1 }'))
+ elif "$HAS_NSLOOKUP"; then
+ ip6=$(filter_ip6_address $(strip_lf "$(nslookup -type=aaaa "$1" 2>/dev/null | awk '/'"^${a}"'.*AAAA/ { print $NF }')"))
+ fi
+ fi
+ OPENSSL_CONF="$saved_openssl_conf" # see https://github.com/drwetter/testssl.sh/issues/134
+ safe_echo "$ip6"
+}
+
+# RFC6844: DNS Certification Authority Authorization (CAA) Resource Record
+# arg1: domain to check for
+get_caa_rr_record() {
+ local raw_caa=""
+ local -i len_caa_property
+ local caa_property_name
+ local caa_property_value
+ local saved_openssl_conf="$OPENSSL_CONF"
+ local all_caa=""
+ local noidnout=""
+
+ "$HAS_DIG_NOIDNOUT" && noidnout="+noidnout"
+
+ [[ -n "$NODNS" ]] && return 0 # if minimum DNS lookup was instructed, leave here
+ # if there's a type257 record there are two output formats here, mostly depending on age of distribution
+ # roughly that's the difference between text and binary format
+ # 1) 'google.com has CAA record 0 issue "symantec.com"'
+ # 2) 'google.com has TYPE257 record \# 19 0005697373756573796D616E7465632E636F6D'
+ # for dig +short the output always starts with '0 issue [..]' or '\# 19 [..]' so we normalize thereto to keep caa_flag, caa_property
+ # caa_property then has key/value pairs, see https://tools.ietf.org/html/rfc6844#section-3
+ OPENSSL_CONF=""
+ if "$HAS_DRILL"; then
+ raw_caa="$(drill $1 type257 | awk '/'"^${1}"'.*CAA/ { print $5,$6,$7 }')"
+ elif "$HAS_HOST"; then
+ raw_caa="$(host -t type257 $1)"
+ if grep -Ewvq "has no CAA|has no TYPE257" <<< "$raw_caa"; then
+ raw_caa="$(sed -e 's/^.*has CAA record //' -e 's/^.*has TYPE257 record //' <<< "$raw_caa")"
+ fi
+ elif "$HAS_DIG"; then
+ raw_caa="$(dig $DIG_R +short +timeout=3 +tries=3 $noidnout type257 "$1" 2>/dev/null | awk '{ print $1" "$2" "$3 }')"
+ # empty if no CAA record
+ elif "$HAS_NSLOOKUP"; then
+ raw_caa="$(strip_lf "$(nslookup -type=type257 $1 | grep -w rdata_257)")"
+ if [[ -n "$raw_caa" ]]; then
+ raw_caa="$(sed 's/^.*rdata_257 = //' <<< "$raw_caa")"
+ fi
+ else
+ return 1
+ # No dig, drill, host, or nslookup --> complaint was elsewhere already
+ fi
+ OPENSSL_CONF="$saved_openssl_conf" # see https://github.com/drwetter/testssl.sh/issues/134
+ debugme echo $raw_caa
+
+ if [[ "$raw_caa" =~ \#\ [0-9][0-9] ]]; then
+ # for posteo we get this binary format returned e.g. for old dig versions:
+ # \# 19 0005697373756567656F74727573742E636F6D
+ # \# 23 0009697373756577696C6467656F74727573742E636F6D
+ # \# 34 0005696F6465666D61696C746F3A686F73746D617374657240706F73 74656F2E6465
+ # # len caaflag <more_see_below> @ p o s t e o . d e
+ while read hash len line ;do
+ if [[ "${line:0:2}" == "00" ]]; then # probably the caa flag, always 00, so we don't keep this
+ len_caa_property=$(printf "%0d" "$((10#${line:2:2}))") # get len and do type casting, for posteo we have 05 or 09 here as a string
+ len_caa_property=$((len_caa_property*2)) # =>word! Now get name from 4th and value from 4th+len position...
+ line="${line/ /}" # especially with iodefs there's a blank in the string which we just skip
+ caa_property_name="$(hex2ascii ${line:4:$len_caa_property})"
+ caa_property_value="$(hex2ascii "${line:$((4+len_caa_property)):100}")"
+ # echo "${caa_property_name}=${caa_property_value}"
+ all_caa+="${caa_property_name}=${caa_property_value}\n"
+ else
+ outln "please report unknown CAA RR $line with flag @ $NODE"
+ return 7
+ fi
+ done <<< "$raw_caa"
+ sort <<< "$(safe_echo "$all_caa")"
+ return 0
+ elif grep -q '"' <<< "$raw_caa"; then
+ raw_caa=${raw_caa//\"/} # strip all ". Now we should have flag, name, value
+ #caa_property_name="$(awk '{ print $2 }' <<< "$raw_caa")"
+ #caa_property_value="$(awk '{ print $3 }' <<< "$raw_caa")"
+ safe_echo "$(sort <<< "$(awk '{ print $2"="$3 }' <<< "$raw_caa")")"
+ return 0
+ else
+ # no caa record
+ return 1
+ fi
+
+# to do:
+# 4: check whether $1 is a CNAME and take this
+ return 0
+}
+
+# arg1: domain
+get_mx_record() {
+ local mxs""
+ local saved_openssl_conf="$OPENSSL_CONF"
+ local noidnout=""
+
+ "$HAS_DIG_NOIDNOUT" && noidnout="+noidnout"
+ OPENSSL_CONF="" # see https://github.com/drwetter/testssl.sh/issues/134
+ # we need the last two columns here
+ if "$HAS_HOST"; then
+ mxs="$(host -t MX "$1" 2>/dev/null | awk '/is handled by/ { print $(NF-1), $NF }')"
+ elif "$HAS_DRILL"; then
+ mxs="$(drill mx $1 | awk '/IN[ \t]MX[ \t]+/ { print $(NF-1), $NF }')"
+ elif "$HAS_DIG"; then
+ mxs="$(dig $DIG_R +short $noidnout -t MX "$1" 2>/dev/null | awk '/^[0-9]/ { print $1" "$2 }')"
+ elif "$HAS_NSLOOKUP"; then
+ mxs="$(strip_lf "$(nslookup -type=MX "$1" 2>/dev/null | awk '/mail exchanger/ { print $(NF-1), $NF }')")"
+ else
+ # shouldn't reach this, as we checked in the top
+ fatal "No dig, host, drill or nslookup" $ERR_DNSBIN
+ fi
+ OPENSSL_CONF="$saved_openssl_conf"
+ echo "$mxs"
+}
+
+
+# set IPADDRs and IP46ADDRs
+#
+determine_ip_addresses() {
+ local ip4=""
+ local ip6=""
+
+ ip4="$(get_a_record "$NODE")"
+ ip6="$(get_aaaa_record "$NODE")"
+ IP46ADDRs=$(newline_to_spaces "$ip4 $ip6")
+
+ if [[ -n "$CMDLINE_IP" ]]; then
+ # command line has supplied an IP address or "one"
+ if [[ "$CMDLINE_IP" == one ]]; then
+ # use first IPv6 or IPv4 address
+ if "$HAS_IPv6" && [[ -n "$ip6" ]]; then
+ CMDLINE_IP="$(head -1 <<< "$ip6")"
+ else
+ CMDLINE_IP="$(head -1 <<< "$ip4")"
+ fi
+ fi
+ NODEIP="$CMDLINE_IP"
+ if is_ipv4addr "$NODEIP"; then
+ ip4="$NODEIP"
+ elif is_ipv6addr "$NODEIP"; then
+ ip6="$NODEIP"
+ else
+ fatal "couldn't identify supplied \"CMDLINE_IP\"" $ERR_DNSLOOKUP
+ fi
+ elif is_ipv4addr "$NODE"; then
+ ip4="$NODE" # only an IPv4 address was supplied as an argument, no hostname
+ SNI="" # override Server Name Indication as we test the IP only
+ else
+ ip4=$(get_local_a "$NODE") # is there a local host entry?
+ if [[ -z "$ip4" ]]; then # empty: no (LOCAL_A is predefined as false)
+ ip4=$(get_a_record "$NODE")
+ else
+ LOCAL_A=true # we have the ip4 from local host entry and need to signal this to testssl
+ fi
+ # same now for ipv6
+ ip6=$(get_local_aaaa "$NODE")
+ if [[ -z "$ip6" ]]; then
+ ip6=$(get_aaaa_record "$NODE")
+ else
+ LOCAL_AAAA=true # we have a local ipv6 entry and need to signal this to testssl
+ fi
+ fi
+
+ # IPv6 only address
+ if [[ -z "$ip4" ]]; then
+ if "$HAS_IPv6"; then
+ IPADDRs=$(newline_to_spaces "$ip6")
+ IP46ADDRs="$IPADDRs" # IP46ADDRs are the ones to display, IPADDRs the ones to test
+ fi
+ else
+ if "$HAS_IPv6" && [[ -n "$ip6" ]]; then
+ if is_ipv6addr "$CMDLINE_IP"; then
+ IPADDRs=$(newline_to_spaces "$ip6")
+ else
+ IPADDRs=$(newline_to_spaces "$ip4 $ip6")
+ fi
+ else
+ IPADDRs=$(newline_to_spaces "$ip4")
+ fi
+ fi
+ if [[ -z "$IPADDRs" ]]; then
+ if [[ -n "$ip6" ]]; then
+ fatal "Only IPv6 address(es) for \"$NODE\" available, maybe add \"-6\" to $0" $ERR_DNSLOOKUP
+ else
+ fatal "No IPv4/IPv6 address(es) for \"$NODE\" available" $ERR_DNSLOOKUP
+ fi
+ fi
+ return 0 # IPADDR and IP46ADDR is set now
+}
+
+determine_rdns() {
+ local saved_openssl_conf="$OPENSSL_CONF"
+ local nodeip="" rdns="" line=""
+
+ [[ -n "$NODNS" ]] && rDNS="(instructed to minimize DNS queries)" && return 0 # PTR records were not asked for
+ local nodeip="$(tr -d '[]' <<< $NODEIP)" # for DNS we do not need the square brackets of IPv6 addresses
+ OPENSSL_CONF="" # see https://github.com/drwetter/testssl.sh/issues/134
+ if [[ "$NODE" == *.local ]]; then
+ if "$HAS_AVAHIRESOLVE"; then
+ rDNS=$(avahi-resolve -a $nodeip 2>/dev/null | awk '{ print $2 }')
+ elif "$HAS_DIG"; then
+ rDNS=$(dig $DIG_R -x $nodeip @224.0.0.251 -p 5353 +notcp +noall +answer +short | awk '{ print $1 }')
+ fi
+ elif "$HAS_HOST"; then
+ rDNS=$(host -t PTR $nodeip 2>/dev/null | awk '/pointer/ { print $NF }')
+ elif "$HAS_DRILL"; then
+ rDNS=$(drill -x ptr $nodeip 2>/dev/null | awk '/ANSWER SECTION/ { getline; print $NF }')
+ elif "$HAS_DIG"; then
+ # 1+2 should suffice. It's a compromise for if e.g. network is down but we have a docker/localhost server
+ rDNS=$(dig $DIG_R -x $nodeip +timeout=1 +tries=2 +noall +answer +short | awk '{ print $1 }') # +short returns also CNAME, e.g. openssl.org
+ elif "$HAS_NSLOOKUP"; then
+ rDNS=$(strip_lf "$(nslookup -type=PTR $nodeip 2>/dev/null | grep -v 'canonical name =' | grep 'name = ' | awk '{ print $NF }' | sed 's/\.$//')")
+ fi
+ OPENSSL_CONF="$saved_openssl_conf" # see https://github.com/drwetter/testssl.sh/issues/134
+ # First, rDNS can contain > 1 line due to multiple PTR DNS records, though this is not recommended.
+ # So we use a loop to check for each FQDN returned. There we remove chars which under weird
+ # circumstances (see #1506) can show up here. The blacklist is taken from RFC 1912 ("Allowable characters in a
+ # label for a host name are only ASCII, letters, digits, and the `-' character")
+ while read -r line; do
+ line="$(tr -dc '[a-zA-Z0-9-_.]' <<< "$line")"
+ [[ -z "$rdns" ]] && rdns="$line" || rdns="$rdns $line"
+ done <<< "$rDNS"
+ rDNS="$rdns"
+ [[ -z "$rDNS" ]] && rDNS="--"
+ return 0
+}
+
+# We need to get the IP address of the proxy so we can use it in fd_socket
+#
+check_proxy() {
+ if [[ -n "$PROXY" ]]; then
+ if ! "$HAS_PROXY"; then
+ fatal "Your $OPENSSL is too old to support the \"-proxy\" option" $ERR_OSSLBIN
+ fi
+ if [[ "$PROXY" == auto ]]; then
+ # Get $ENV https_proxy is the one we care about for connects
+ PROXY="${https_proxy#*\/\/}"
+ # Fallback:
+ [[ -z "$PROXY" ]] && PROXY="${http_proxy#*\/\/}"
+ [[ -z "$PROXY" ]] && fatal "you specified \"--proxy=auto\" but \"\$http(s)_proxy\" is empty" $ERR_CMDLINE
+ fi
+ # strip off http/https part if supplied:
+ PROXY="${PROXY/http\:\/\//}"
+ PROXY="${PROXY/https\:\/\//}" # this shouldn't be needed
+ PROXYNODE="${PROXY%:*}"
+ PROXYPORT="${PROXY#*:}"
+ is_number "$PROXYPORT" || fatal "Proxy port cannot be determined from \"$PROXY\"" $ERR_CMDLINE
+
+ #if is_ipv4addr "$PROXYNODE" || is_ipv6addr "$PROXYNODE" ; then
+ # IPv6 via openssl -proxy: that doesn't work. Sockets does
+#FIXME: finish this with LibreSSL which supports an IPv6 proxy
+ if is_ipv4addr "$PROXYNODE"; then
+ PROXYIP="$PROXYNODE"
+ else
+ PROXYIP="$(get_a_record "$PROXYNODE" 2>/dev/null | grep -v alias | sed 's/^.*address //')"
+ [[ -z "$PROXYIP" ]] && fatal "Proxy IP cannot be determined from \"$PROXYNODE\"" $ERR_CMDLINE
+ fi
+ PROXY="-proxy $PROXYIP:$PROXYPORT"
+ fi
+}
+
+
+# this is only being called from determine_optimal_proto in order to check whether we have a server
+# with client authentication, a server with no SSL session ID switched off
+#
+sclient_auth() {
+ [[ $1 -eq 0 ]] && return 0 # no client auth (CLIENT_AUTH=false is preset globally)
+ if [[ -n $(awk '/Master-Key: / { print $2 }' "$2") ]]; then # connect succeeded
+ if grep -q '^<<< .*CertificateRequest' "$2"; then # CertificateRequest message in -msg
+ CLIENT_AUTH=true
+ return 0
+ fi
+ if [[ -z $(awk '/Session-ID: / { print $2 }' "$2") ]]; then # probably no SSL session
+ if [[ 2 -eq $(grep -c CERTIFICATE "$2") ]]; then # do another sanity check to be sure
+ CLIENT_AUTH=false
+ NO_SSL_SESSIONID=true # NO_SSL_SESSIONID is preset globally to false for all other cases
+ return 0
+ fi
+ fi
+ fi
+ # what's left now is: master key empty, handshake returned not successful, session ID empty --> not successful
+ return 1
+}
+
+# Determine the best parameters to use with tls_sockets():
+# For TLSv1.3, determine what extension number to use for the key_share extension.
+# For TLSv1.2, determine what cipher list to send, since there are more than 128
+# TLSv1.2 ciphers and some servers fail if the ClientHello contains too many ciphers.
+# If both TLSv1.3 and TLSv1.2 ClientHello messages result in failed connection attempts,
+# then try to determine whether:
+# (1) This is an SSLv2-only server
+# (2) This server supports some protocol in SSLv3 - TLSv1.1, but cannot handle version negotiation.
+# (3) This is not a TLS/SSL enabled server.
+# This information can be used by determine_optimal_proto() to help distinguish between a server
+# that is not TLS/SSL enabled and one that is not compatible with the version of OpenSSL being used.
+determine_optimal_sockets_params() {
+ local -i ret1=1 ret2=1
+ local i proto cipher_offered
+ local all_failed=true
+
+ # If a STARTTLS protocol is specified and $SSL_NATIVE is true, then skip this test, since
+ # $SSL_NATIVE may have been set to true as a result of tls_sockets() not supporting the STARTTLS
+ # protocol.
+ [[ -n "$STARTTLS_PROTOCOL" ]] && "$SSL_NATIVE" && return 0
+
+ # NOTE: The following code is only needed as long as draft versions of TLSv1.3 prior to draft 23
+ # are supported. It is used to determine whether a draft 23 or pre-draft 23 ClientHello should be
+ # sent.
+ KEY_SHARE_EXTN_NR="33"
+ tls_sockets "04" "$TLS13_CIPHER" "" "00, 2b, 00, 0f, 0e, 03,04, 7f,1c, 7f,1b, 7f,1a, 7f,19, 7f,18, 7f,17"
+ if [[ $? -eq 0 ]]; then
+ add_tls_offered tls1_3 yes
+ all_failed=false
+ else
+ KEY_SHARE_EXTN_NR="28"
+ tls_sockets "04" "$TLS13_CIPHER" "" "00, 2b, 00, 0b, 0a, 7f,16, 7f,15, 7f,14, 7f,13, 7f,12"
+ if [[ $? -eq 0 ]]; then
+ add_tls_offered tls1_3 yes
+ all_failed=false
+ else
+ add_tls_offered tls1_3 no
+ KEY_SHARE_EXTN_NR="33"
+ fi
+ fi
+ if ! "$all_failed"; then
+ # Determine which version of TLS 1.3 was offered. For drafts 18-21 the
+ # version appears in the ProtocolVersion field of the ServerHello. For
+ # drafts 22-28 and the final TLS 1.3 the ProtocolVersion field contains
+ # 0303 and the actual version appears in the supported_versions extension.
+ if [[ "${TLS_SERVER_HELLO:8:3}" == 7F1 ]]; then
+ add_tls_offered tls1_3_draft$(hex2dec "${TLS_SERVER_HELLO:10:2}") yes
+ elif [[ "$TLS_SERVER_HELLO" =~ 002B00020304 ]]; then
+ add_tls_offered tls1_3_rfc8446 yes
+ elif [[ "$TLS_SERVER_HELLO" =~ 002B00027F1[2-9A-C] ]]; then
+ add_tls_offered tls1_3_draft$(hex2dec "${BASH_REMATCH:10:2}") yes
+ fi
+ fi
+
+ # Need to determine which set of ciphers is best to use with
+ # a TLSv1.2 ClientHello since there are far more than 128 ciphers
+ # that can be used.
+ tls_sockets "03" "$TLS12_CIPHER"
+ ret1=$?
+ if [[ $ret1 -eq 0 ]] || [[ $ret1 -eq 2 ]]; then
+ case $DETECTED_TLS_VERSION in
+ 0303) add_tls_offered tls1_2 yes ;;
+ 0302) add_tls_offered tls1_1 yes ;;
+ 0301) add_tls_offered tls1 yes ;;
+ 0300) add_tls_offered ssl3 yes ;;
+ esac
+ all_failed=false
+ fi
+
+ # Try again with a different, less common, set of cipher suites
+ # see #807 and #806. If using these cipher suites results in a
+ # successful connection, then change $TLS12_CIPHER to these
+ # cipher suites so that later tests will use this list of cipher
+ # suites.
+ if [[ $ret1 -ne 0 ]]; then
+ tls_sockets "03" "$TLS12_CIPHER_2ND_TRY"
+ ret2=$?
+ if [[ $ret2 -eq 0 ]]; then
+ add_tls_offered tls1_2 yes
+ TLS12_CIPHER="$TLS12_CIPHER_2ND_TRY"
+ all_failed=false
+ else
+ add_tls_offered tls1_2 no
+ fi
+ if [[ $ret2 -eq 2 ]]; then
+ case $DETECTED_TLS_VERSION in
+ 0302) add_tls_offered tls1_1 yes ;;
+ 0301) add_tls_offered tls1 yes ;;
+ 0300) add_tls_offered ssl3 yes ;;
+ esac
+ [[ $ret1 -ne 2 ]] && TLS12_CIPHER="$TLS12_CIPHER_2ND_TRY"
+ all_failed=false
+ fi
+ fi
+ if [[ $ret1 -eq 0 ]] || [[ $ret2 -eq 0 ]]; then
+ cipher_offered="$(get_cipher "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt")"
+ if [[ "$cipher_offered" == TLS_* ]] || [[ "$cipher_offered" == SSL_* ]]; then
+ cipher_offered="$(rfc2hexcode "$cipher_offered")"
+ else
+ cipher_offered="$(openssl2hexcode "$cipher_offered")"
+ fi
+ [[ ${#cipher_offered} -eq 9 ]] && TLS12_CIPHER_OFFERED="${cipher_offered:2:2},${cipher_offered:7:2}"
+ fi
+
+ if "$all_failed"; then
+ # One of the following must be true:
+ # * This is not a TLS/SSL enabled server.
+ # * The server only supports SSLv2
+ # * The server does not handle version negotiation correctly.
+ for proto in 01 00 02; do
+ tls_sockets "$proto" "$TLS_CIPHER" "" "" "true"
+ ret1=$?
+ if [[ $ret1 -ne 0 ]]; then
+ case $proto in
+ 02) add_tls_offered tls1_1 no ;;
+ 01) add_tls_offered tls1 no ;;
+ 00) add_tls_offered ssl3 no ;;
+ esac
+ fi
+ if [[ $ret1 -eq 0 ]] || [[ $ret1 -eq 2 ]]; then
+ case $DETECTED_TLS_VERSION in
+ 0302) add_tls_offered tls1_1 yes ;;
+ 0301) add_tls_offered tls1 yes ;;
+ 0300) add_tls_offered ssl3 yes ;;
+ esac
+ OPTIMAL_SOCKETS_PROTO="$proto"
+ all_failed=false
+ break
+ fi
+ done
+ fi
+ if "$all_failed"; then
+ sslv2_sockets
+ [[ $? -eq 3 ]] && all_failed=false && add_tls_offered ssl2 yes
+ fi
+ ALL_FAILED_SOCKETS="$all_failed"
+ return 0
+}
+
+
+# This function determines (STARTTLS_)OPTIMAL_PROTO. It is basically a workaround function as under certain
+# circumstances a ClientHello without specifying a protocol will fail.
+# Circumstances observed so far: 1.) IIS 6 and openssl 1.0.2 as opposed to 1.0.1 2.) starttls + dovecot imap.
+# Independent on the server side it seems reasonable to to know upfront which protocol always works
+#
+# arg1: if empty: no STARTTLS, else: STARTTLS protocol
+# The first try in the loop is empty as we prefer not to specify always a protocol if we can get along w/o it
+#
+determine_optimal_proto() {
+ local all_failed=true
+ local tmp=""
+ local proto optimal_proto
+
+ "$do_tls_sockets" && return 0
+
+ >$ERRFILE
+ if [[ -n "$1" ]]; then
+ # STARTTLS workaround needed see https://github.com/drwetter/testssl.sh/issues/188 -- kind of odd
+ for STARTTLS_OPTIMAL_PROTO in -tls1_2 -tls1 -ssl3 -tls1_1 -tls1_3 -ssl2; do
+ case $STARTTLS_OPTIMAL_PROTO in
+ -tls1_3) "$HAS_TLS13" || continue ;;
+ -ssl3) "$HAS_SSL3" || continue ;;
+ -ssl2) "$HAS_SSL2" || continue ;;
+ *) ;;
+ esac
+ $OPENSSL s_client $(s_client_options "$STARTTLS_OPTIMAL_PROTO $BUGS -connect "$NODEIP:$PORT" $PROXY -msg $STARTTLS $SNI") </dev/null >$TMPFILE 2>>$ERRFILE
+ if sclient_auth $? $TMPFILE; then
+ all_failed=false
+ add_tls_offered "${STARTTLS_OPTIMAL_PROTO/-/}" yes
+ break
+ fi
+ done
+ "$all_failed" && STARTTLS_OPTIMAL_PROTO=""
+ optimal_proto="$STARTTLS_OPTIMAL_PROTO"
+ debugme echo "STARTTLS_OPTIMAL_PROTO: $STARTTLS_OPTIMAL_PROTO"
+ else
+ # No STARTTLS
+ for proto in '' -tls1_2 -tls1 -tls1_3 -ssl3 -tls1_1 -ssl2; do
+ case $proto in
+ -tls1_3) "$HAS_TLS13" || continue ;;
+ -ssl3) "$HAS_SSL3" || continue ;;
+ -ssl2) "$HAS_SSL2" || continue ;;
+ *) ;;
+ esac
+ $OPENSSL s_client $(s_client_options "$proto $BUGS -connect "$NODEIP:$PORT" -msg $PROXY $SNI") </dev/null >$TMPFILE 2>>$ERRFILE
+ if sclient_auth $? $TMPFILE; then
+ # we use the successful handshake at least to get one valid protocol supported -- it saves us time later
+ if [[ -z "$proto" ]]; then
+ # convert to openssl terminology
+ tmp=$(get_protocol $TMPFILE)
+ tmp=${tmp/\./_}
+ tmp=${tmp/v/}
+ tmp="$(tolower $tmp)"
+ add_tls_offered "${tmp}" yes
+ debugme echo "one proto determined: $tmp"
+ OPTIMAL_PROTO=""
+ else
+ add_tls_offered "${proto/-/}" yes
+ OPTIMAL_PROTO="$proto"
+ fi
+ all_failed=false
+ break
+ fi
+ done
+ "$all_failed" && OPTIMAL_PROTO=""
+ optimal_proto="$OPTIMAL_PROTO"
+
+ debugme echo "OPTIMAL_PROTO: $OPTIMAL_PROTO"
+ fi
+ [[ "$optimal_proto" != -ssl2 ]] && ! "$all_failed" && grep -q '^Server Temp Key' $TMPFILE && HAS_DH_BITS=true # FIX #190
+ if [[ "$(has_server_protocol "tls1_3")" -eq 0 ]] && [[ "$(has_server_protocol "tls1_2")" -ne 0 ]] &&
+ [[ "$(has_server_protocol "tls1_1")" -ne 0 ]] && [[ "$(has_server_protocol "tls1")" -ne 0 ]] &&
+ [[ "$(has_server_protocol "ssl3")" -ne 0 ]]; then
+ TLS13_ONLY=true
+ fi
+
+ if [[ "$optimal_proto" == -ssl2 ]]; then
+ prln_magenta "$NODEIP:$PORT appears to only support SSLv2."
+ ignore_no_or_lame " Type \"yes\" to proceed and accept false negatives or positives" "yes"
+ [[ $? -ne 0 ]] && exit $ERR_CLUELESS
+ elif "$all_failed" && ! "$ALL_FAILED_SOCKETS"; then
+ if ! "$HAS_TLS13" && "$TLS13_ONLY"; then
+ pr_magenta " $NODE:$PORT appears to support TLS 1.3 ONLY. You better use --openssl=<path_to_openssl_supporting_TLS_1.3>"
+ if ! "$OSSL_SHORTCUT" || [[ ! -x /usr/bin/openssl ]] || /usr/bin/openssl s_client -tls1_3 -connect invalid. 2>&1 | grep -aiq "unknown option"; then
+ outln
+ ignore_no_or_lame " Type \"yes\" to proceed and accept all scan problems" "yes"
+ [[ $? -ne 0 ]] && exit $ERR_CLUELESS
+ MAX_OSSL_FAIL=10
+ else
+ # dirty hack but an idea for the future to be implemented upfront: Now we know, we'll better off
+ # with the OS supplied openssl binary. We need to inittialize variables / arrays again though.
+ # And the service detection can't be made up for now
+ outln ", \n proceeding with /usr/bin/openssl"
+ OPENSSL=/usr/bin/openssl
+ find_openssl_binary
+ prepare_arrays
+ fi
+ elif ! "$HAS_SSL3" && [[ "$(has_server_protocol "ssl3")" -eq 0 ]] && [[ "$(has_server_protocol "tls1_3")" -ne 0 ]] && \
+ [[ "$(has_server_protocol "tls1_2")" -ne 0 ]] && [[ "$(has_server_protocol "tls1_1")" -ne 0 ]] &&
+ [[ "$(has_server_protocol "tls1")" -ne 0 ]]; then
+ prln_magenta " $NODE:$PORT appears to support SSLv3 ONLY. You better use --openssl=<path_to_openssl_supporting_SSL_3>"
+ ignore_no_or_lame " Type \"yes\" to proceed and accept all scan problems" "yes"
+ [[ $? -ne 0 ]] && exit $ERR_CLUELESS
+ MAX_OSSL_FAIL=10
+ else
+ prln_bold " Your OpenSSL cannot connect to $NODEIP:$PORT"
+ ignore_no_or_lame " The results might look ok but they could be nonsense. Really proceed ? (\"yes\" to continue)" "yes"
+ [[ $? -ne 0 ]] && exit $ERR_CLUELESS
+ fi
+ elif "$all_failed"; then
+ outln
+ if "$HAS_IPv6"; then
+ pr_bold " Your $OPENSSL is not IPv6 aware, or $NODEIP:$PORT "
+ else
+ pr_bold " $NODEIP:$PORT "
+ fi
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ prln_bold "doesn't seem to be a TLS/SSL enabled server";
+ ignore_no_or_lame " The results might look ok but they could be nonsense. Really proceed ? (\"yes\" to continue)" "yes"
+ [[ $? -ne 0 ]] && exit $ERR_CLUELESS
+ elif ! "$all_failed" && "$ALL_FAILED_SOCKETS" && ! "$SSL_NATIVE"; then
+ # For some reason connecting with tls_sockets/sslv2_sockets didn't work, but connecting
+ # with $OPENSSL s_client did.
+ # FIXME: Should we include some sort of "please report" note here?
+ prln_magenta " Testing with $NODE:$PORT only worked using $OPENSSL."
+ prln_magenta " Test results may be somewhat better if the --ssl-native option is used."
+ ignore_no_or_lame " Type \"yes\" to proceed and accept false negatives or positives" "yes"
+ [[ $? -ne 0 ]] && exit $ERR_CLUELESS
+ fi
+
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 0
+}
+
+
+# arg1 (optional): ftp smtp, lmtp, pop3, imap, xmpp, telnet, ldap, postgres, mysql, irc, nntp (maybe with trailing s)
+#
+determine_service() {
+ local ua
+ local protocol
+ local basicauth_header=""
+
+ # Check if we can connect to $NODEIP:$PORT. Attention: This ALWAYS uses sockets. Thus timeouts for --ssl-=native do not apply
+ if ! fd_socket 5; then
+ if [[ -n "$PROXY" ]]; then
+ fatal "You're sure $PROXYNODE:$PROXYPORT allows tunneling here? Can't connect to \"$NODEIP:$PORT\"" $ERR_CONNECT
+ else
+ if "$MULTIPLE_CHECKS"; then
+ ip_fatal "Couldn't connect to $NODEIP:$PORT"
+ return 1
+ else
+ fatal "Can't connect to \"$NODEIP:$PORT\"\nMake sure a firewall is not between you and your scanning target!" $ERR_CONNECT
+ fi
+ fi
+ fi
+ close_socket
+
+ outln
+ if [[ -z "$1" ]]; then
+ # no STARTTLS.
+ determine_optimal_sockets_params
+ determine_optimal_proto
+ $SNEAKY && \
+ ua="$UA_SNEAKY" || \
+ ua="$UA_STD"
+ if [[ -n "$BASICAUTH" ]]; then
+ basicauth_header="Authorization: Basic $(safe_echo "$BASICAUTH" | $OPENSSL base64 2>/dev/null)\r\n"
+ fi
+ GET_REQ11="GET $URL_PATH HTTP/1.1\r\nHost: $NODE\r\nUser-Agent: $ua\r\n${basicauth_header}Accept-Encoding: identity\r\nAccept: text/*\r\nConnection: Close\r\n\r\n"
+ # returns always 0:
+ service_detection $OPTIMAL_PROTO
+ else # STARTTLS
+ if [[ "$1" == postgres ]]; then
+ protocol="postgres"
+ else
+ protocol=${1%s} # strip trailing 's' in ftp(s), smtp(s), pop3(s), etc
+ fi
+
+ case "$protocol" in
+ ftp|smtp|lmtp|pop3|imap|xmpp|telnet|ldap|postgres|mysql|nntp)
+ STARTTLS="-starttls $protocol"
+ if [[ "$protocol" == xmpp ]]; then
+ if [[ -n "$XMPP_HOST" ]]; then
+ if ! "$HAS_XMPP"; then
+ fatal "Your $OPENSSL does not support the \"-xmpphost\" option" $ERR_OSSLBIN
+ fi
+ STARTTLS="$STARTTLS -xmpphost $XMPP_HOST" # small hack -- instead of changing calls all over the place
+ # see https://xmpp.org/rfcs/rfc3920.html
+ else
+ if is_ipv4addr "$NODE"; then
+ # XMPP needs a jabber domainname
+ if [[ -n "$rDNS" ]]; then
+ prln_warning " IP address doesn't work for XMPP, trying PTR record $rDNS"
+ # remove trailing .
+ NODE=${rDNS%%.}
+ else
+ fatal "No DNS supplied and no PTR record available which I can try for XMPP" $ERR_DNSLOOKUP
+ fi
+ fi
+ if "$HAS_XMPP"; then
+ # small hack -- instead of changing calls all over the place
+ STARTTLS="$STARTTLS -xmpphost $NODE"
+ else
+ # If the XMPP name cannot be provided using -xmpphost,
+ # then it needs to be provided to the -connect option
+ NODEIP="$NODE"
+ fi
+ fi
+ elif [[ "$protocol" == postgres ]]; then
+ # Check if openssl version supports postgres.
+ if ! "$HAS_POSTGRES"; then
+ fatal "Your $OPENSSL does not support the \"-starttls postgres\" option" $ERR_OSSLBIN
+ fi
+ elif [[ "$protocol" == mysql ]]; then
+ # Check if openssl version supports mysql.
+ if ! "$HAS_MYSQL"; then
+ fatal "Your $OPENSSL does not support the \"-starttls mysql\" option" $ERR_OSSLBIN
+ fi
+ elif [[ "$protocol" == lmtp ]]; then
+ # Check if openssl version supports lmtp.
+ if ! "$HAS_LMTP"; then
+ fatal "Your $OPENSSL does not support the \"-starttls lmtp\" option" $ERR_OSSLBIN
+ fi
+ elif [[ "$protocol" == nntp ]]; then
+ # Check if openssl version supports lmtp.
+ if ! "$HAS_NNTP"; then
+ fatal "Your $OPENSSL does not support the \"-starttls nntp\" option" $ERR_OSSLBIN
+ fi
+ fi
+ determine_optimal_sockets_params
+ determine_optimal_proto "$1"
+
+ out " Service set:$CORRECT_SPACES STARTTLS via "
+ out "$(toupper "$protocol")"
+ [[ "$protocol" == mysql ]] && out " (experimental)"
+ fileout "service" "INFO" "$protocol"
+ [[ -n "$XMPP_HOST" ]] && out " (XMPP domain=\'$XMPP_HOST\')"
+ outln
+ ;;
+ *) outln
+ fatal "momentarily only ftp, smtp, lmtp, pop3, imap, xmpp, telnet, ldap, nntp, postgres and mysql allowed" $ERR_CMDLINE
+ ;;
+ esac
+ fi
+ tmpfile_handle ${FUNCNAME[0]}.txt
+ return 0 # OPTIMAL_PROTO, GET_REQ*/HEAD_REQ* is set now
+}
+
+
+# Sets SERVER_SIZE_LIMIT_BUG to true or false, depending on whether we hit the 128 cipher limit.
+# Return value is 0 unless we have a problem executing
+#
+determine_sizelimitbug() {
+ # overflow_cipher must be some cipher that does not appear in TLS12_CIPHER.
+ local overflow_cipher='C0,86'
+ local -i nr_ciphers
+
+ # For STARTTLS protocols not being implemented yet via sockets this is a bypass otherwise it won't be usable at all (e.g. LDAP)
+ # Fixme: find out whether we can't skip this in general for STARTTLS
+ [[ "$STARTTLS" =~ ldap ]] && return 0
+ [[ "$STARTTLS" =~ irc ]] && return 0
+
+ # Only with TLS 1.2 offered at the server side it is possible to hit this bug, in practise. Thus
+ # we assume if TLS 1.2 is not supported, the server has no cipher size limit bug. It still may,
+ # theoretically, but in a regular check with testssl.sh we won't hit this limit with lower protocols.
+ # Upon calling this function we already know whether TLS 1.2 is supported. If TLS 1.2 is supported, we
+ # send 129 ciphers (including 00FF) and check whether it works.
+
+ if [[ 1 -eq $(has_server_protocol 03) ]]; then
+ SERVER_SIZE_LIMIT_BUG=false
+ else
+ if [[ "$DEBUG" -ge 1 ]]; then
+ nr_ciphers="$(tr ' ' '\n' <<< "${overflow_cipher}, $TLS12_CIPHER" | sed -e '/^$/d' | wc -l)"
+ if [[ $nr_ciphers -ne 129 ]]; then
+ prln_warning "FIXME line $LINENO, ${FUNCNAME[0]} sending $nr_ciphers ciphers rather than 129."
+ else
+ debugme echo "${FUNCNAME[0]} sending $nr_ciphers ciphers"
+ fi
+ fi
+ tls_sockets 03 "${overflow_cipher}, ${TLS12_CIPHER}"
+ if [[ $? -eq 0 ]]; then
+ SERVER_SIZE_LIMIT_BUG=false
+ else
+ SERVER_SIZE_LIMIT_BUG=true
+ fi
+ debugme echo -e "\nSERVER_SIZE_LIMIT_BUG: $SERVER_SIZE_LIMIT_BUG"
+ fi
+ if "$SERVER_SIZE_LIMIT_BUG"; then
+ out " Pre-test: "
+ prln_svrty_medium "128 cipher limit bug"
+ fileout "pre_128cipher" "MEDIUM" "128 cipher limit bug"
+ else
+ [[ "$DEBUG" -ge 1 ]] && outln " Pre-test: No 128 cipher limit bug"
+ fileout "pre_128cipher" "INFO" "No 128 cipher limit bug"
+ fi
+ return 0
+}
+
+
+display_rdns_etc() {
+ local ip further_ip_addrs=""
+ local nodeip="$(tr -d '[]' <<< $NODEIP)" # for displaying IPv6 addresses we don't need []
+
+ if [[ -n "$PROXY" ]]; then
+ out " Via Proxy: $CORRECT_SPACES"
+ outln "$PROXYIP:$PROXYPORT "
+ fi
+ if [[ $(count_words "$IP46ADDRs") -gt 1 ]]; then
+ out " Further IP addresses: $CORRECT_SPACES"
+ for ip in $IP46ADDRs; do
+ if [[ "$ip" == "$NODEIP" ]] || [[ "[$ip]" == "$NODEIP" ]]; then
+ continue
+ else
+ further_ip_addrs+="$ip "
+ fi
+ done
+ outln "$(out_row_aligned_max_width "$further_ip_addrs" " $CORRECT_SPACES" $TERM_WIDTH)"
+ fi
+ if "$LOCAL_A"; then
+ outln " A record via: $CORRECT_SPACES /etc/hosts "
+ elif "$LOCAL_AAAA"; then
+ outln " AAAA record via: $CORRECT_SPACES /etc/hosts "
+ elif [[ -n "$CMDLINE_IP" ]]; then
+ if is_ipv6addr $"$CMDLINE_IP"; then
+ outln " AAAA record via: $CORRECT_SPACES supplied IP \"$CMDLINE_IP\""
+ else
+ outln " A record via: $CORRECT_SPACES supplied IP \"$CMDLINE_IP\""
+ fi
+ fi
+ if [[ "$rDNS" =~ instructed ]]; then
+ out "$(printf " %-23s " "rDNS ($nodeip):")"
+ out "$rDNS"
+ elif [[ -n "$rDNS" ]]; then
+ out "$(printf " %-23s " "rDNS ($nodeip):")"
+ out "$(out_row_aligned_max_width "$rDNS" " $CORRECT_SPACES" $TERM_WIDTH)"
+ fi
+}
+
+datebanner() {
+ local scan_time_f=""
+
+ if [[ "$1" =~ Done ]] ; then
+ scan_time_f="$(printf "%04ss" "$SCAN_TIME")" # 4 digits because of windows
+ pr_reverse "$1 $(date +%F) $(date +%T) [$scan_time_f] -->> $NODEIP:$PORT ($NODE) <<--"
+ else
+ pr_reverse "$1 $(date +%F) $(date +%T) -->> $NODEIP:$PORT ($NODE) <<--"
+ fi
+ outln "\n"
+ [[ "$1" =~ Start ]] && display_rdns_etc
+}
+
+# one line with char $1 over screen width $2
+draw_line() {
+ out "$(printf -- "$1"'%.s' $(eval "echo {1.."$(($2))"}"))"
+}
+
+
+run_mx_all_ips() {
+ local mxs mx
+ local mxport
+ local -i ret=0
+ local word=""
+
+ STARTTLS_PROTOCOL="smtp"
+ # test first higher priority servers
+ mxs=$(get_mx_record "$1" | sort -n | sed -e 's/^.* //' -e 's/\.$//' | tr '\n' ' ')
+ if [[ $CMDLINE_IP == one ]]; then
+ word="as instructed one" # with highest priority
+ mxs=${mxs%% *}
+ else
+ word="the only"
+ fi
+ mxport=${2:-25}
+ if [[ -n "$LOGFILE" ]]; then
+ prepare_logging
+ else
+ prepare_logging "${FNAME_PREFIX}mx-$1"
+ fi
+ if [[ -n "$mxs" ]] && [[ "$mxs" != ' ' ]]; then
+ [[ $(count_words "$mxs") -gt 1 ]] && MULTIPLE_CHECKS=true
+ if "$MULTIPLE_CHECKS"; then
+ pr_bold "Testing all MX records (on port $mxport): "
+ else
+ pr_bold "Testing $word MX record (on port $mxport): "
+ fi
+ outln "$mxs"
+ [[ $mxport == 465 ]] && STARTTLS_PROTOCOL="" # no starttls for tcp 465, all other ports are starttls
+ for mx in $mxs; do
+ draw_line "-" $((TERM_WIDTH * 2 / 3))
+ outln
+ parse_hn_port "$mx:$mxport"
+ determine_ip_addresses || continue
+ if [[ $(count_words "$IPADDRs") -gt 1 ]]; then # we have more than one ipv4 address to check
+ MULTIPLE_CHECKS=true
+ pr_bold "Testing all IPv4 addresses (port $PORT): "; outln "$IPADDRs"
+ for ip in $IPADDRs; do
+ NODEIP="$ip"
+ lets_roll "${STARTTLS_PROTOCOL}"
+ done
+ else
+ NODEIP="$IPADDRs"
+ lets_roll "${STARTTLS_PROTOCOL}"
+ fi
+ ret=$(($? + ret))
+ done
+ draw_line "-" $((TERM_WIDTH * 2 / 3))
+ outln
+ pr_bold "Done testing all MX records (on port $mxport): "; outln "$mxs"
+ else
+ prln_bold " $1 has no MX records(s)"
+ fi
+ return $ret
+}
+
+# If run_mass_testing() is being used, then create the command line
+# for the test based on the global command line (all elements of the
+# command line provided to the parent, except the --file/-iL option) and the
+# specific command line options for the test to be run. Each argument
+# in the command line needs to be a separate element in an array in order
+# to deal with word splitting within file names (see #702).
+#
+# If run_mass_testing_parallel() is being used, then in addition to the above,
+# modify global command line for child tests so that if all (JSON, CSV, HTML)
+# output is to go into a single file, each child will have its output placed in
+# a separate, named file, so that the separate files can be concatenated
+# together once they are complete to create the single file.
+#
+# If run_mass_testing() is being used, then "$1" is "serial". If
+# run_mass_testing_parallel() is being used, then "$1" is "parallel XXXXXXXX"
+# where XXXXXXXX is the number of the test being run.
+#
+create_mass_testing_cmdline() {
+ local testing_type="$1"
+ local cmd test_number
+ local outfile_arg
+ local -i nr_cmds=0 index=0
+ local skip_next=false
+
+ MASS_TESTING_CMDLINE=()
+ [[ "$testing_type" =~ parallel ]] && read -r testing_type test_number <<< "$testing_type"
+
+ # Start by adding the elements from the global command line to the command line for the
+ # test. If run_mass_testing_parallel(), then modify the command line so that, when
+ # required, each child process sends its test results to a separate file. If a cmd
+ # uses '=' for supplying a value we just skip next parameter (we don't use 'parse_opt_equal_sign' here)
+ debugme echo "${CMDLINE_ARRAY[@]}"
+ for cmd in "${CMDLINE_ARRAY[@]}"; do
+ "$skip_next" && skip_next=false && index+=1 && continue
+ if [[ "$cmd" =~ --file ]] || [[ "$cmd" =~ -iL ]]; then
+ # Don't include the "--file[=...] or -iL argument in the child's command
+ # line, but do include "--warnings=batch".
+ MASS_TESTING_CMDLINE[nr_cmds]="--warnings=batch"
+ nr_cmds+=1
+ # next is the file itself, as no '=' was supplied
+ [[ "$cmd" == --file ]] && skip_next=true
+ [[ "$cmd" == -iL ]] && skip_next=true
+ elif [[ "$testing_type" == serial ]]; then
+ if "$JSONHEADER" && ( [[ "$cmd" =~ --jsonfile-pretty ]] || [[ "$cmd" =~ -oJ ]] ); then
+ >"$TEMPDIR/jsonfile_child.json"
+ MASS_TESTING_CMDLINE[nr_cmds]="--jsonfile-pretty=$TEMPDIR/jsonfile_child.json"
+ # next is the jsonfile itself, as no '=' was supplied
+ [[ "$cmd" == --jsonfile-pretty ]] && skip_next=true
+ [[ "$cmd" == -oJ ]] && skip_next=true
+ elif "$JSONHEADER" && ( [[ "$cmd" =~ --jsonfile ]] || [[ "$cmd" =~ -oj ]] ); then
+ >"$TEMPDIR/jsonfile_child.json"
+ MASS_TESTING_CMDLINE[nr_cmds]="--jsonfile=$TEMPDIR/jsonfile_child.json"
+ # next is the jsonfile itself, as no '=' was supplied
+ [[ "$cmd" == --jsonfile ]] && skip_next=true
+ [[ "$cmd" == -oj ]] && skip_next=true
+ elif "$JSONHEADER" && ( [[ "$cmd" =~ --outFile ]] || [[ "$cmd" =~ -oA ]] ); then
+ outfile_arg="$(parse_opt_equal_sign "$cmd" "${CMDLINE_ARRAY[index+1]}")"
+ >"$TEMPDIR/jsonfile_child.json"
+ MASS_TESTING_CMDLINE[nr_cmds]="-oJ=$TEMPDIR/jsonfile_child.json"
+ nr_cmds+=1
+ MASS_TESTING_CMDLINE[nr_cmds]="-oC=$outfile_arg.csv"
+ nr_cmds+=1
+ MASS_TESTING_CMDLINE[nr_cmds]="-oH=$outfile_arg.html"
+ # next is the filename itself, as no '=' was supplied
+ [[ "$cmd" == --outFile ]] && skip_next=true
+ [[ "$cmd" == -oA ]] && skip_next=true
+ elif "$JSONHEADER" && ( [[ "$cmd" =~ --outfile ]] || [[ "$cmd" =~ -oa ]] ); then
+ outfile_arg="$(parse_opt_equal_sign "$cmd" "${CMDLINE_ARRAY[index+1]}")"
+ >"$TEMPDIR/jsonfile_child.json"
+ MASS_TESTING_CMDLINE[nr_cmds]="-oj=$TEMPDIR/jsonfile_child.json"
+ nr_cmds+=1
+ MASS_TESTING_CMDLINE[nr_cmds]="-oC=$outfile_arg.csv"
+ nr_cmds+=1
+ MASS_TESTING_CMDLINE[nr_cmds]="-oH=$outfile_arg.html"
+ # next is the filename itself, as no '=' was supplied
+ [[ "$cmd" == --outfile ]] && skip_next=true
+ [[ "$cmd" == -oa ]] && skip_next=true
+ else
+ MASS_TESTING_CMDLINE[nr_cmds]="$cmd"
+ fi
+ nr_cmds+=1
+ else
+ case "$cmd" in
+ --jsonfile|--jsonfile=*|-oj|-oj=*)
+ # If <jsonfile> is a file, then have provide a different
+ # file name to each child process. If <jsonfile> is a
+ # directory, then just pass it on to the child processes.
+ if "$JSONHEADER"; then
+ MASS_TESTING_CMDLINE[nr_cmds]="--jsonfile=$TEMPDIR/jsonfile_${test_number}.json"
+ # next is the jsonfile itself, as no '=' was supplied
+ [[ "$cmd" == --jsonfile ]] && skip_next=true
+ [[ "$cmd" == -oj ]] && skip_next=true
+ else
+ MASS_TESTING_CMDLINE[nr_cmds]="$cmd"
+ fi
+ ;;
+ --jsonfile-pretty|--jsonfile-pretty=*|-oJ|-oJ=*)
+ if "$JSONHEADER"; then
+ MASS_TESTING_CMDLINE[nr_cmds]="--jsonfile-pretty=$TEMPDIR/jsonfile_${test_number}.json"
+ [[ "$cmd" == --jsonfile-pretty ]] && skip_next=true
+ [[ "$cmd" == -oJ ]] && skip_next=true
+ else
+ MASS_TESTING_CMDLINE[nr_cmds]="$cmd"
+ fi
+ ;;
+ --csvfile|--csvfile=*|-oC|-oC=*)
+ if "$CSVHEADER"; then
+ MASS_TESTING_CMDLINE[nr_cmds]="--csvfile=$TEMPDIR/csvfile_${test_number}.csv"
+ [[ "$cmd" == --csvfile ]] && skip_next=true
+ [[ "$cmd" == -oC ]] && skip_next=true
+ else
+ MASS_TESTING_CMDLINE[nr_cmds]="$cmd"
+ fi
+ ;;
+ --htmlfile|--htmlfile=*|-oH|-oH=*)
+ if "$HTMLHEADER"; then
+ MASS_TESTING_CMDLINE[nr_cmds]="--htmlfile=$TEMPDIR/htmlfile_${test_number}.html"
+ [[ "$cmd" == --htmlfile ]] && skip_next=true
+ [[ "$cmd" == -oH ]] && skip_next=true
+ else
+ MASS_TESTING_CMDLINE[nr_cmds]="$cmd"
+ fi
+ ;;
+ --outfile|--outfile=*|-oa|-oa=*)
+ if "$JSONHEADER"; then
+ MASS_TESTING_CMDLINE[nr_cmds]="-oj=$TEMPDIR/jsonfile_${test_number}.json"
+ nr_cmds+=1
+ MASS_TESTING_CMDLINE[nr_cmds]="-oC=$TEMPDIR/csvfile_${test_number}.csv"
+ nr_cmds+=1
+ MASS_TESTING_CMDLINE[nr_cmds]="-oH=$TEMPDIR/htmlfile_${test_number}.html"
+ # next is the filename itself, as no '=' was supplied
+ [[ "$cmd" == --outfile ]] && skip_next=true
+ [[ "$cmd" == -oa ]] && skip_next=true
+ else
+ MASS_TESTING_CMDLINE[nr_cmds]="$cmd"
+ fi
+ ;;
+ --outFile|--outFile=*|-oA|-oA=*)
+ if "$JSONHEADER"; then
+ MASS_TESTING_CMDLINE[nr_cmds]="-oJ=$TEMPDIR/jsonfile_${test_number}.json"
+ nr_cmds+=1
+ MASS_TESTING_CMDLINE[nr_cmds]="-oC=$TEMPDIR/csvfile_${test_number}.csv"
+ nr_cmds+=1
+ MASS_TESTING_CMDLINE[nr_cmds]="-oH=$TEMPDIR/htmlfile_${test_number}.html"
+ # next is the filename itself, as no '=' was supplied
+ [[ "$cmd" == --outFile ]] && skip_next=true
+ [[ "$cmd" == -oA ]] && skip_next=true
+ else
+ MASS_TESTING_CMDLINE[nr_cmds]="$cmd"
+ fi
+ ;;
+ *)
+ MASS_TESTING_CMDLINE[nr_cmds]="$cmd"
+ ;;
+ esac
+ nr_cmds+=1
+ fi
+ index+=1
+ done
+
+ # Now add the command line arguments for the specific test to the command line.
+ # Skip the first argument sent to this function, since it specifies the type of testing being performed.
+ shift
+ while [[ $# -gt 0 ]]; do
+ MASS_TESTING_CMDLINE[nr_cmds]="$1"
+ nr_cmds+=1
+ shift
+ done
+
+ return 0
+}
+
+
+ports2starttls() {
+ local tcp_port=$1
+ local ret=0
+
+ # https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers
+ case $tcp_port in
+ 21) echo "-t ftp " ;;
+ 23) echo "-t telnet " ;;
+ 119|433) echo "-t nntp " ;; # to come
+ 25|587) echo "-t smtp " ;;
+ 110) echo "-t pop3 " ;;
+ 143) echo "-t imap " ;;
+ 389) echo "-t ldap ";;
+ 3306) echo "-t mysql " ;;
+ 5222) echo "-t xmpp " ;; # domain of jabber server maybe needed
+ 5432) echo "-t postgres " ;;
+ 563) ;; # NNTPS
+ 636) ;; # LDAP
+ 1443|8443|443|981) ;; # HTTPS
+ 465) ;; # HTTPS | SMTP
+ 631) ;; # CUPS
+ 853) ;; # DNS over TLS
+ 995|993) ;; # POP3|IMAP
+ 3389) ;; # RDP
+ *) ret=1 ;; # we don't know this ports so we rather do not scan it
+ esac
+ return $ret
+}
+
+nmap_to_plain_file() {
+ local target_fname=""
+ local oneline=""
+ local ip hostdontcare round_brackets ports_specs starttls
+ local tmp port host_spec protocol ssl_hint dontcare dontcare1
+ #FIXME: IPv6 is missing here
+
+ # Ok, since we are here we are sure to have an nmap file. To avoid questions we make sure it's the right format too
+ if [[ "$(head -1 "$FNAME")" =~ ( -oG )(.*) ]] || [[ "$(head -1 "$FNAME")" =~ ( -oA )(.*) ]] ; then
+ # yes, greppable
+ if [[ $(grep -c Status "$FNAME") -ge 1 ]]; then
+ [[ $(grep -c '\/open\/' "$FNAME") -eq 0 ]] && \
+ fatal "Nmap file $FNAME should contain at least one open port" $ERR_FNAMEPARSE
+ else
+ fatal "strange, nmap grepable misses \"Status\"" -1
+ fi
+ else
+ fatal "Nmap file $FNAME is not in grep(p)able format (-oG filename.g(n)map)" $ERR_FNAMEPARSE
+ fi
+ # create ${FNAME%.*}.txt in $TEMPDIR
+ target_fname="${FNAME%.*}.txt"
+ target_fname="${target_fname##*\/}" # strip path (Unix)
+ target_fname="${target_fname##*\\}" # strip path (Dos)
+ target_fname="$TEMPDIR/$target_fname"
+ > "${target_fname}" || fatal "Cannot create \"${target_fname}\"" $ERR_FCREATE
+
+ # Line x: "Host: AAA.BBB.CCC.DDD (<FQDN>) Status: Up"
+ # Line x+1: "Host: AAA.BBB.CCC.DDD (<FQDN>) Ports: 443/open/tcp//https///"
+ # (or): Host: AAA.BBB.CCC.DDD (<FQDN>) Ports: 22/open/tcp//ssh//<banner>/, 25/open/tcp//smtp//<banner>/, 443/open/tcp//ssl|http//<banner>
+ while read -r hostdontcare ip round_brackets tmp ports_specs; do
+ [[ "$ports_specs" =~ "Status: " ]] && continue # we don't need this
+ [[ "$ports_specs" =~ '/open/tcp/' ]] || continue # no open tcp at all for this IP --> move
+ host_spec="$ip"
+ fqdn="${round_brackets/\(/}"
+ fqdn="${fqdn/\)/}"
+ if [[ -n "$fqdn" ]]; then
+ tmp="$(get_a_record "$fqdn")"
+ debugme echo "$tmp \?= $ip"
+ if [[ "$tmp" == "$ip" ]]; then
+ host_spec="$fqdn"
+ fi
+ fi
+ while read -r oneline; do
+ # 25/open/tcp//smtp//<banner>/,
+ [[ "$oneline" =~ '/open/tcp/' ]] || continue # no open tcp for this port on this IP --> move on
+ IFS=/ read -r port dontcare protocol ssl_hint dontcare1 <<< "$oneline"
+ if [[ "$ssl_hint" =~ ^(ssl|https) ]] || [[ "$dontcare1" =~ ^(ssl|https) ]]; then
+ echo "${host_spec}:${port}" >>"$target_fname"
+ else
+ starttls="$(ports2starttls $port)"
+ [[ $? -eq 1 ]] && continue # nmap got a port but we don't know how to speak to
+ [[ "$DEBUG" -ge 1 ]] && echo "${starttls}$host_spec:$port"
+ echo "${starttls}${host_spec}:${port}" >>"$target_fname"
+ fi
+ done < <(tr ',' '\n' <<< "$ports_specs")
+ done < "$FNAME"
+ [[ "$DEBUG" -ge 1 ]] && echo
+
+ [[ -s "$target_fname" ]] || \
+ fatal "Couldn't find any open port in $FNAME" $ERR_FNAMEPARSE
+ export FNAME=$target_fname
+}
+
+run_mass_testing() {
+ local cmdline=""
+ local first=true
+ local gnmapadd=""
+ local saved_fname="$FNAME"
+
+ if [[ ! -r "$FNAME" ]] && "$IKNOW_FNAME"; then
+ fatal "Can't read file \"$FNAME\"" $ERR_FNAMEPARSE
+ fi
+
+ if [[ "$(head -1 "$FNAME")" =~ (Nmap [4-8])(.*)( scan initiated )(.*) ]]; then
+ gnmapadd="grep(p)able nmap "
+ nmap_to_plain_file
+ fi
+
+ pr_reverse "====== Running in file batch mode with ${gnmapadd}file=\"$saved_fname\" ======"; outln "\n"
+ while read -r cmdline; do
+ cmdline="$(filter_input "$cmdline")"
+ [[ -z "$cmdline" ]] && continue
+ [[ "$cmdline" == EOF ]] && break
+ # Create the command line for the child in the form of an array (see #702)
+ create_mass_testing_cmdline "serial" $cmdline
+ draw_line "=" $((TERM_WIDTH / 2)); outln;
+ outln "$(create_cmd_line_string "$0" "${MASS_TESTING_CMDLINE[@]}")"
+ # we call ourselves here. $do_mass_testing is the parent, $CHILD_MASS_TESTING... you figured
+ if [[ -z "$(type -p "$0")" ]]; then
+ CHILD_MASS_TESTING=true "$RUN_DIR/$PROG_NAME" "${MASS_TESTING_CMDLINE[@]}"
+ else
+ CHILD_MASS_TESTING=true "$0" "${MASS_TESTING_CMDLINE[@]}"
+ fi
+ if "$JSONHEADER" && [[ -s "$TEMPDIR/jsonfile_child.json" ]]; then
+ # Need to ensure that a separator is only added if the test
+ # produced some JSON output.
+ "$first" || fileout_separator # this is needed for appended output, see #687
+ first=false
+ cat "$TEMPDIR/jsonfile_child.json" >> "$JSONFILE"
+ FIRST_FINDING=false
+ fi
+ done < "${FNAME}"
+ return $?
+}
+
+# This function is called when it has been determined that the next child
+# process has completed or it has been stopped. If the child process completed,
+# then this process prints the child process's output to the terminal and, if
+# appropriate, adds any JSON, CSV, and HTML output it has created to the
+# appropriate file. If the child process was stopped, then a message indicating
+# that is printed, but the incomplete results are not used.
+#
+get_next_message_testing_parallel_result() {
+ draw_line "=" $((TERM_WIDTH / 2)); outln;
+ outln "${PARALLEL_TESTING_CMDLINE[NEXT_PARALLEL_TEST_TO_FINISH]}"
+ if [[ "$1" == completed ]]; then
+ cat "$TEMPDIR/term_output_$(printf "%08d" $NEXT_PARALLEL_TEST_TO_FINISH).log"
+ if "$JSONHEADER" && [[ -s "$TEMPDIR/jsonfile_$(printf "%08d" $NEXT_PARALLEL_TEST_TO_FINISH).json" ]]; then
+ # Need to ensure that a separator is only added if the test
+ # produced some JSON output.
+ "$FIRST_JSON_OUTPUT" || fileout_separator # this is needed for appended output, see #687
+ FIRST_JSON_OUTPUT=false
+ FIRST_FINDING=false
+ cat "$TEMPDIR/jsonfile_$(printf "%08d" $NEXT_PARALLEL_TEST_TO_FINISH).json" >> "$JSONFILE"
+ fi
+ "$CSVHEADER" && cat "$TEMPDIR/csvfile_$(printf "%08d" $NEXT_PARALLEL_TEST_TO_FINISH).csv" >> "$CSVFILE"
+ "$HTMLHEADER" && cat "$TEMPDIR/htmlfile_$(printf "%08d" $NEXT_PARALLEL_TEST_TO_FINISH).html" >> "$HTMLFILE"
+ elif [[ "$1" == "stopped" ]]; then
+ outln "\nTest was stopped before it completed.\n"
+ else
+ outln "\nTest timed out before it completed.\n"
+ fi
+}
+
+#FIXME: not called/tested yet
+run_mass_testing_parallel() {
+ local cmdline=""
+ local -i i nr_active_tests=0
+ local -a -i start_time=()
+ local -i curr_time wait_time
+ local gnmapadd=""
+ local saved_fname="$FNAME"
+
+ if [[ ! -r "$FNAME" ]] && $IKNOW_FNAME; then
+ fatal "Can't read file \"$FNAME\"" $ERR_FNAMEPARSE
+ fi
+
+ if [[ "$(head -1 "$FNAME")" =~ (Nmap [4-8])(.*)( scan initiated )(.*) ]]; then
+ gnmapadd="grep(p)able nmap "
+ nmap_to_plain_file
+ fi
+
+ pr_reverse "====== Running in file batch mode with ${gnmapadd}file=\"$saved_fname\" ======"; outln "\n"
+ while read -r cmdline; do
+ cmdline="$(filter_input "$cmdline")"
+ [[ -z "$cmdline" ]] && continue
+ [[ "$cmdline" == "EOF" ]] && break
+ # Create the command line for the child in the form of an array (see #702)
+ create_mass_testing_cmdline "parallel $(printf "%08d" $NR_PARALLEL_TESTS)" $cmdline
+
+ # fileout() won't include the "service" information in the JSON file for the child process
+ # if the JSON file doesn't already exist.
+ "$JSONHEADER" && >"$TEMPDIR/jsonfile_$(printf "%08d" $NR_PARALLEL_TESTS).json"
+ PARALLEL_TESTING_CMDLINE[NR_PARALLEL_TESTS]="$(create_cmd_line_string "$0" "${MASS_TESTING_CMDLINE[@]}")"
+ if [[ -z "$(type -p "$0")" ]]; then
+ CHILD_MASS_TESTING=true "$RUN_DIR/$PROG_NAME" "${MASS_TESTING_CMDLINE[@]}" > "$TEMPDIR/term_output_$(printf "%08d" $NR_PARALLEL_TESTS).log" 2>&1 &
+ else
+ CHILD_MASS_TESTING=true "$0" "${MASS_TESTING_CMDLINE[@]}" > "$TEMPDIR/term_output_$(printf "%08d" $NR_PARALLEL_TESTS).log" 2>&1 &
+ fi
+ PARALLEL_TESTING_PID[NR_PARALLEL_TESTS]=$!
+ start_time[NR_PARALLEL_TESTS]=$(date +%s)
+ if "$INTERACTIVE"; then
+ echo -en "\r \r" 1>&2
+ echo -n "Started test #$NR_PARALLEL_TESTS" 1>&2
+ [[ $NEXT_PARALLEL_TEST_TO_FINISH -lt $NR_PARALLEL_TESTS ]] && \
+ echo -n " (waiting for test #$NEXT_PARALLEL_TEST_TO_FINISH to finish)" 1>&2
+ fi
+ NR_PARALLEL_TESTS+=1
+ nr_active_tests+=1
+ sleep $PARALLEL_SLEEP
+ # Get the results of any completed tests
+ while [[ $NEXT_PARALLEL_TEST_TO_FINISH -lt $NR_PARALLEL_TESTS ]]; do
+ if [[ ${PARALLEL_TESTING_PID[NEXT_PARALLEL_TEST_TO_FINISH]} -eq 0 ]]; then
+ "$INTERACTIVE" && echo -en "\r \r" 1>&2
+ get_next_message_testing_parallel_result "completed"
+ NEXT_PARALLEL_TEST_TO_FINISH+=1
+ elif ! ps ${PARALLEL_TESTING_PID[NEXT_PARALLEL_TEST_TO_FINISH]} >/dev/null ; then
+ "$INTERACTIVE" && echo -en "\r \r" 1>&2
+ get_next_message_testing_parallel_result "completed"
+ NEXT_PARALLEL_TEST_TO_FINISH+=1
+ nr_active_tests=$nr_active_tests-1
+ else
+ break
+ fi
+ done
+ if [[ $nr_active_tests -ge $MAX_PARALLEL ]]; then
+ curr_time=$(date +%s)
+ while true; do
+ # Check to see if any test completed
+ for (( i=NEXT_PARALLEL_TEST_TO_FINISH; i < NR_PARALLEL_TESTS; i++ )); do
+ if [[ ${PARALLEL_TESTING_PID[i]} -ne 0 ]] && \
+ ! ps ${PARALLEL_TESTING_PID[i]} >/dev/null ; then
+ PARALLEL_TESTING_PID[i]=0
+ nr_active_tests=$nr_active_tests-1
+ break
+ fi
+ done
+ [[ $nr_active_tests -lt $MAX_PARALLEL ]] && break
+ if [[ $curr_time-${start_time[NEXT_PARALLEL_TEST_TO_FINISH]} -ge $MAX_WAIT_TEST ]]; then
+ # No test completed in the allocated time, so the first one to
+ # start will be killed.
+ kill ${PARALLEL_TESTING_PID[NEXT_PARALLEL_TEST_TO_FINISH]} >&2 2>/dev/null
+ wait ${PARALLEL_TESTING_PID[NEXT_PARALLEL_TEST_TO_FINISH]} 2>/dev/null # make sure pid terminated, see wait(1p)
+ "$INTERACTIVE" && echo -en "\r \r" 1>&2
+ get_next_message_testing_parallel_result "timeout"
+ NEXT_PARALLEL_TEST_TO_FINISH+=1
+ nr_active_tests=$nr_active_tests-1
+ break
+ fi
+ # Wake up to increment the counter every second (so that the counter
+ # appears to users as if it is operating smoothly), but check the
+ # status of the $MAX_PARALLEL active processes less often, since the
+ # ps command is expensive.
+ for (( i=0; i <= $((MAX_PARALLEL/5)); i++ )); do
+ wait_time=$((curr_time-start_time[NEXT_PARALLEL_TEST_TO_FINISH]))
+ [[ $wait_time -gt $MAX_WAIT_TEST ]] && wait_time=$MAX_WAIT_TEST
+ if "$INTERACTIVE"; then
+ echo -en "\r \r" 1>&2
+ echo -n "Waiting for test #$NEXT_PARALLEL_TEST_TO_FINISH to finish" 1>&2
+ if [[ $((MAX_WAIT_TEST-wait_time)) -le 60 ]]; then
+ echo -n " ($((MAX_WAIT_TEST-wait_time)) seconds to timeout)" 1>&2
+ else
+ echo -n " ($wait_time seconds)" 1>&2
+ fi
+ fi
+ [[ $wait_time -ge $MAX_WAIT_TEST ]] && break
+ sleep 1
+ curr_time=$(date +%s)
+ done
+ done
+ fi
+ done < "$FNAME"
+
+ # Wait for remaining tests to finish
+ curr_time=$(date +%s)
+ while [[ $NEXT_PARALLEL_TEST_TO_FINISH -lt $NR_PARALLEL_TESTS ]]; do
+ if [[ ${PARALLEL_TESTING_PID[NEXT_PARALLEL_TEST_TO_FINISH]} -eq 0 ]] || \
+ ! ps ${PARALLEL_TESTING_PID[NEXT_PARALLEL_TEST_TO_FINISH]} >/dev/null ; then
+ "$INTERACTIVE" && echo -en "\r \r" 1>&2
+ get_next_message_testing_parallel_result "completed"
+ NEXT_PARALLEL_TEST_TO_FINISH+=1
+ elif [[ $curr_time-${start_time[NEXT_PARALLEL_TEST_TO_FINISH]} -ge $MAX_WAIT_TEST ]]; then
+ kill ${PARALLEL_TESTING_PID[NEXT_PARALLEL_TEST_TO_FINISH]} >&2 2>/dev/null
+ wait ${PARALLEL_TESTING_PID[NEXT_PARALLEL_TEST_TO_FINISH]} 2>/dev/null # make sure pid terminated, see wait(1p)
+ "$INTERACTIVE" && echo -en "\r \r" 1>&2
+ get_next_message_testing_parallel_result "timeout"
+ NEXT_PARALLEL_TEST_TO_FINISH+=1
+ else
+ # Here it is okay to check process status every second, since the
+ # status of only one process is being checked.
+ if "$INTERACTIVE"; then
+ echo -en "\r \r" 1>&2
+ wait_time=$((curr_time-start_time[NEXT_PARALLEL_TEST_TO_FINISH]))
+ [[ $wait_time -gt $MAX_WAIT_TEST ]] && wait_time=$MAX_WAIT_TEST
+ echo -n "Waiting for test #$NEXT_PARALLEL_TEST_TO_FINISH to finish" 1>&2
+ if [[ $((MAX_WAIT_TEST-wait_time)) -le 60 ]]; then
+ echo -n " ($((MAX_WAIT_TEST-wait_time)) seconds to timeout)" 1>&2
+ else
+ echo -n " ($wait_time seconds)" 1>&2
+ fi
+ fi
+ sleep 1
+ curr_time=$(date +%s)
+ fi
+ done
+ return $?
+}
+
+
+
+# This initializes boolean global do_* variables. They keep track of what to do
+# -- as the name insinuates
+initialize_globals() {
+ do_allciphers=false
+ do_vulnerabilities=false
+ do_beast=false
+ do_lucky13=false
+ do_breach=false
+ do_ccs_injection=false
+ do_ticketbleed=false
+ do_robot=false
+ do_cipher_per_proto=false
+ do_crime=false
+ do_freak=false
+ do_logjam=false
+ do_drown=false
+ do_header=false
+ do_heartbleed=false
+ do_mx_all_ips=false
+ do_mass_testing=false
+ do_logging=false
+ do_json=false
+ do_pretty_json=false
+ do_csv=false
+ do_html=false
+ do_pfs=false
+ do_protocols=false
+ do_rc4=false
+ do_grease=false
+ do_renego=false
+ do_cipherlists=false
+ do_server_defaults=false
+ do_server_preference=false
+ do_ssl_poodle=false
+ do_sweet32=false
+ do_tls_fallback_scsv=false
+ do_cipher_match=false
+ do_tls_sockets=false
+ do_client_simulation=false
+ do_display_only=false
+ do_starttls=false
+}
+
+
+# Set default scanning options for the boolean global do_* variables.
+set_scanning_defaults() {
+ do_allciphers=true
+ do_vulnerabilities=true
+ do_beast=true
+ do_lucky13=true
+ do_breach=true
+ do_heartbleed="$OFFENSIVE"
+ do_ccs_injection="$OFFENSIVE"
+ do_ticketbleed="$OFFENSIVE"
+ do_robot="$OFFENSIVE"
+ do_crime=true
+ do_freak=true
+ do_logjam=true
+ do_drown=true
+ do_ssl_poodle=true
+ do_sweet32=true
+ do_header=true
+ do_pfs=true
+ do_rc4=true
+ do_protocols=true
+ do_renego=true
+ do_cipherlists=true
+ do_server_defaults=true
+ do_server_preference=true
+ do_tls_fallback_scsv=true
+ do_client_simulation=true
+ if "$OFFENSIVE"; then
+ VULN_COUNT=16
+ else
+ VULN_COUNT=12
+ fi
+}
+
+# returns number of $do variables set = number of run_funcs() to perform
+count_do_variables() {
+ local gbl
+ local true_nr=0
+
+ for gbl in do_allciphers do_vulnerabilities do_beast do_lucky13 do_breach do_ccs_injection do_ticketbleed do_cipher_per_proto do_crime \
+ do_freak do_logjam do_drown do_header do_heartbleed do_mx_all_ips do_pfs do_protocols do_rc4 do_grease do_robot do_renego \
+ do_cipherlists do_server_defaults do_server_preference do_ssl_poodle do_tls_fallback_scsv \
+ do_sweet32 do_client_simulation do_cipher_match do_tls_sockets do_mass_testing do_display_only; do
+ [[ "${!gbl}" == true ]] && let true_nr++
+ done
+ return $true_nr
+}
+
+
+debug_globals() {
+ local gbl
+
+ for gbl in do_allciphers do_vulnerabilities do_beast do_lucky13 do_breach do_ccs_injection do_ticketbleed do_cipher_per_proto do_crime \
+ do_freak do_logjam do_drown do_header do_heartbleed do_mx_all_ips do_pfs do_protocols do_rc4 do_grease do_robot do_renego \
+ do_cipherlists do_server_defaults do_server_preference do_ssl_poodle do_tls_fallback_scsv \
+ do_sweet32 do_client_simulation do_cipher_match do_tls_sockets do_mass_testing do_display_only; do
+ printf "%-22s = %s\n" $gbl "${!gbl}"
+ done
+ printf "%-22s : %s\n" URI: "$URI"
+}
+
+
+# arg1: either switch+value (=) or switch
+# arg2: value (if no = provided)
+parse_opt_equal_sign() {
+ if [[ "$1" == *=* ]]; then
+ echo ${1#*=}
+ return 1 # = means we don't need to shift args!
+ else
+ echo "$2"
+ return 0 # we need to shift
+ fi
+}
+
+# Create the command line string for printing purposes
+# See https://stackoverflow.com/questions/10835933/preserve-quotes-in-bash-arguments
+create_cmd_line_string() {
+ local arg
+ local -a allargs=()
+ local chars='[ !"#$&()*,;<>?\^`{|}]'
+
+ while [[ $# -gt 0 ]]; do
+ if [[ $1 == *\'* ]]; then
+ arg=\""$1"\"
+ elif [[ $1 == *$chars* ]]; then
+ arg="'$1'"
+ else
+ arg="$1"
+ fi
+ allargs+=("$arg") # ${allargs[@]} is to be used only for printing
+ shift
+ done
+ printf '%s\n' "${allargs[*]}"
+}
+
+check_base_requirements() {
+ local binary=''
+ local whitelist=' hexdump grep awk sed '
+
+ for binary in 'hexdump' 'dd' 'grep' 'awk' 'tr' 'sed' 'wc' 'date' 'cat' 'ps' 'kill' 'head' 'tail' 'dirname'; do
+ if ! type -p "${binary}" &> /dev/null; then
+ fatal "You need to install ${binary} for this program to work" $ERR_RESOURCE
+ fi
+ [[ ${whitelist} =~ \ ${binary}\ ]] && continue
+ "${binary}" --help 2>&1 | grep -iq busybox
+ if [[ $? -eq 0 ]]; then
+ fatal "${binary} is from busybox. Please install a regular binary" $ERR_RESOURCE
+ fi
+ done
+}
+
+parse_cmd_line() {
+ local outfile_arg=""
+ local cipher_mapping
+ local -i subret=0
+
+ CMDLINE="$(create_cmd_line_string "${CMDLINE_ARRAY[@]}")"
+ CMDLINE_PARSED=false
+
+ case $1 in
+ --help|"")
+ help 0
+ ;;
+ -b|--banner|-v|--version)
+ maketempf
+ get_install_dir
+ find_openssl_binary
+ prepare_debug
+ mybanner
+ exit $ALLOK
+ ;;
+ esac
+
+ # initializing
+ initialize_globals
+
+ while [[ $# -gt 0 ]]; do
+ case $1 in
+ --mx)
+ do_mx_all_ips=true
+ PORT=25
+ ;;
+ --mx465) # doesn't work with major ISPs
+ do_mx_all_ips=true
+ PORT=465
+ ;;
+ --mx587) # doesn't work with major ISPs
+ do_mx_all_ips=true
+ PORT=587
+ ;;
+ --ip|--ip=*)
+ CMDLINE_IP="$(parse_opt_equal_sign "$1" "$2")"
+ [[ $? -eq 0 ]] && shift
+ if [[ "$CMDLINE_IP" == proxy ]]; then
+ DNS_VIA_PROXY=true
+ unset CMDLINE_IP
+ fi
+ # normalize any IPv6 address
+ CMDLINE_IP="${CMDLINE_IP//[/}" # fix vim syntax highlighting "]
+ CMDLINE_IP="${CMDLINE_IP//]/}"
+ ;;
+ -n|--nodns|-n=*|--nodns=*)
+ NODNS="$(parse_opt_equal_sign "$1" "$2")"
+ [[ $? -eq 0 ]] && shift
+ if [[ "$NODNS" != none ]] && [[ "$NODNS" != min ]]; then
+ fatal "Value for nodns switch can be either \"min\" or \"none\"" $ERR_CMDLINE
+ fi
+ ;;
+ -V|-V=*|--local|--local=*) # attention, this could have a value or not!
+ do_display_only=true
+ PATTERN2SHOW="$(parse_opt_equal_sign "$1" "$2")"
+ subret=$?
+ if [[ "$PATTERN2SHOW" == -* ]]; then
+ unset PATTERN2SHOW # we hit the next command ==> not our value
+ else # it was ours, point to next arg
+ [[ $subret -eq 0 ]] && shift
+ fi
+ ;;
+ -x|-x=*|--single[-_]cipher|--single[-_]cipher=*)
+ do_cipher_match=true
+ single_cipher=$(parse_opt_equal_sign "$1" "$2")
+ [[ $? -eq 0 ]] && shift
+ ;;
+ -t|-t=*|--starttls|--starttls=*)
+ do_starttls=true
+ STARTTLS_PROTOCOL="$(parse_opt_equal_sign "$1" "$2")"
+ [[ $? -eq 0 ]] && shift
+ case $STARTTLS_PROTOCOL in
+ ftp|smtp|lmtp|pop3|imap|xmpp|telnet|ldap|irc|nntp|postgres|mysql) ;;
+ ftps|smtps|lmtps|pop3s|imaps|xmpps|telnets|ldaps|ircs|nntps|mysqls) ;;
+ *) tmln_magenta "\nunrecognized STARTTLS protocol \"$1\", see help" 1>&2
+ help 1 ;;
+ esac
+ ;;
+ --xmpphost|--xmpphost=*)
+ XMPP_HOST=$(parse_opt_equal_sign "$1" "$2")
+ [[ $? -eq 0 ]] && shift
+ ;;
+ -e|--each-cipher)
+ do_allciphers=true
+ ;;
+ -E|--cipher-per-proto|--cipher_per_proto)
+ do_cipher_per_proto=true
+ ;;
+ -p|--protocols)
+ do_protocols=true
+ ;;
+ -s|--std|--standard)
+ do_cipherlists=true
+ ;;
+ -S|--server[-_]defaults)
+ do_server_defaults=true
+ ;;
+ -P|--server[_-]preference|--preference)
+ do_server_preference=true
+ ;;
+ -h|--header|--headers)
+ do_header=true
+ ;;
+ -c|--client-simulation)
+ do_client_simulation=true
+ ;;
+ -U|--vulnerable|--vulnerabilities)
+ # Lookahead function: If the order of the cmdline is '-U --ids-friendly'
+ # then we need to make sure we catch --ids-friendly. Normally we do not,
+ # see #1717. The following statement makes sure. In the do-while + case-esac
+ # loop it will be execute again, but it does not hurt
+ if [[ "${CMDLINE_ARRAY[@]}" =~ --ids-friendly ]]; then
+ OFFENSIVE=false
+ fi
+ do_vulnerabilities=true
+ do_heartbleed="$OFFENSIVE"
+ do_ccs_injection="$OFFENSIVE"
+ do_ticketbleed="$OFFENSIVE"
+ do_robot="$OFFENSIVE"
+ do_renego=true
+ do_crime=true
+ do_breach=true
+ do_ssl_poodle=true
+ do_tls_fallback_scsv=true
+ do_sweet32=true
+ do_freak=true
+ do_drown=true
+ do_logjam=true
+ do_beast=true
+ do_lucky13=true
+ do_rc4=true
+ if "$OFFENSIVE"; then
+ VULN_COUNT=16
+ else
+ VULN_COUNT=12
+ fi
+ ;;
+ --ids-friendly)
+ OFFENSIVE=false
+ ;;
+ -H|--heartbleed)
+ do_heartbleed=true
+ let "VULN_COUNT++"
+ ;;
+ -I|--ccs|--ccs[-_]injection)
+ do_ccs_injection=true
+ let "VULN_COUNT++"
+ ;;
+ -T|--ticketbleed)
+ do_ticketbleed=true
+ let "VULN_COUNT++"
+ ;;
+ -BB|--robot)
+ do_robot=true
+ ;;
+ -R|--renegotiation)
+ do_renego=true
+ let "VULN_COUNT++"
+ ;;
+ -C|--compression|--crime)
+ do_crime=true
+ let "VULN_COUNT++"
+ ;;
+ -B|--breach)
+ do_breach=true
+ let "VULN_COUNT++"
+ ;;
+ -O|--poodle)
+ do_ssl_poodle=true
+ do_tls_fallback_scsv=true
+ let "VULN_COUNT++"
+ ;;
+ -Z|--tls[_-]fallback|tls[_-]fallback[_-]scs)
+ do_tls_fallback_scsv=true
+ let "VULN_COUNT++"
+ ;;
+ -W|--sweet32)
+ do_sweet32=true
+ let "VULN_COUNT++"
+ ;;
+ -F|--freak)
+ do_freak=true
+ let "VULN_COUNT++"
+ ;;
+ -D|--drown)
+ do_drown=true
+ let "VULN_COUNT++"
+ ;;
+ -J|--logjam)
+ do_logjam=true
+ let "VULN_COUNT++"
+ ;;
+ -A|--beast)
+ do_beast=true
+ let "VULN_COUNT++"
+ ;;
+ -L|--lucky13)
+ do_lucky13=true
+ let "VULN_COUNT++"
+ ;;
+ -4|--rc4|--appelbaum)
+ do_rc4=true
+ let "VULN_COUNT++"
+ ;;
+ -f|--pfs|--fs|--nsa)
+ do_pfs=true
+ ;;
+ -g|--grease)
+ do_grease=true
+ ;;
+ -9|--full)
+ set_scanning_defaults
+ do_allciphers=false
+ do_cipher_per_proto=true
+ do_grease=true
+ ;;
+ --add-ca|--add-CA|--add-ca=*|--add-CA=*)
+ ADDITIONAL_CA_FILES="$(parse_opt_equal_sign "$1" "$2")"
+ [[ $? -eq 0 ]] && shift
+ ;;
+ --devel) ### this development feature will soon disappear
+ # arg1: SSL/TLS protocol (SSLv2=22)
+ # arg2: list of cipher suites / hostname/ip
+ # arg3: hostname/ip
+ HEX_CIPHER="$TLS12_CIPHER"
+ # DEBUG=3 ./testssl.sh --devel 04 "13,02, 13,01" google.com --> TLS 1.3
+ # DEBUG=3 ./testssl.sh --devel 03 "cc, 13, c0, 13" google.de --> TLS 1.2, old CHACHA/POLY
+ # DEBUG=3 ./testssl.sh --devel 03 "cc,a8, cc,a9, cc,aa, cc,ab, cc,ac" blog.cloudflare.com --> new CHACHA/POLY
+ # DEBUG=3 ./testssl.sh --devel 01 yandex.ru --> TLS 1.0
+ # DEBUG=3 ./testssl.sh --devel 00 <host which supports SSLv3>
+ # DEBUG=3 ./testssl.sh --devel 22 <host which still supports SSLv2>
+ TLS_LOW_BYTE="$2";
+ if [[ $# -eq 4 ]]; then # protocol AND ciphers specified
+ HEX_CIPHER="$3"
+ shift
+ fi
+ shift
+ do_tls_sockets=true
+ outln "\nTLS_LOW_BYTE, HEX_CIPHER: \"${TLS_LOW_BYTE}\", \"${HEX_CIPHER}\""
+ ;;
+ --wide)
+ WIDE=true
+ ;;
+ --assuming[_-]http|--assume[-_]http)
+ ASSUME_HTTP=true
+ ;;
+ --sneaky)
+ SNEAKY=true
+ ;;
+ -q|--quiet)
+ QUIET=true
+ ;;
+ --file|--file=*|-iL|-iL=*)
+ # no shift here as otherwise URI is empty and it bails out
+ FNAME="$(parse_opt_equal_sign "$1" "$2")"
+ [[ $? -eq 0 ]] && shift
+ IKNOW_FNAME=true
+ WARNINGS=batch # set this implicitly!
+ do_mass_testing=true
+ ;;
+ --mode|--mode=*)
+ MASS_TESTING_MODE="$(parse_opt_equal_sign "$1" "$2")"
+ [[ $? -eq 0 ]] && shift
+ case "$MASS_TESTING_MODE" in
+ serial|parallel) ;;
+ *) tmln_magenta "\nmass testing mode can be either \"serial\" or \"parallel\"" 1>&2
+ help 1
+ esac
+ ;;
+ --serial)
+ MASS_TESTING_MODE=serial
+ ;;
+ --parallel)
+ MASS_TESTING_MODE=parallel
+ ;;
+ --warnings|--warnings=*)
+ WARNINGS=$(parse_opt_equal_sign "$1" "$2")
+ [[ $? -eq 0 ]] && shift
+ case "$WARNINGS" in
+ batch|off) ;;
+ *) tmln_magenta "\nwarnings can be either \"batch\", or \"off\"" 1>&2
+ help 1
+ esac
+ ;;
+ --show[-_]each)
+ SHOW_EACH_C=true
+ ;;
+ --fast)
+ FAST=true
+ ;;
+ --bugs)
+ BUGS="-bugs"
+ ;;
+ --debug|--debug=*)
+ DEBUG=$(parse_opt_equal_sign "$1" "$2")
+ [[ $? -eq 0 ]] && shift
+ case $DEBUG in
+ [0-6]) ;;
+ *) tmln_magenta "\nunrecognized debug value \"$1\", must be between 0..6" 1>&2
+ help 1
+ esac
+ ;;
+ --color|--color=*)
+ COLOR="$(parse_opt_equal_sign "$1" "$2")"
+ [[ $? -eq 0 ]] && shift
+ case $COLOR in
+ [0-3]) ;;
+ *) COLOR=2
+ tmln_magenta "\nunrecognized color: \"$1\", must be between 0..3" 1>&2
+ help 1
+ esac
+ ;;
+ --colorblind)
+ COLORBLIND=true
+ ;;
+ --log|--logging)
+ "$do_logging" && fatal "two --log* arguments" $ERR_CMDLINE
+ do_logging=true
+ ;; # DEFINITION of LOGFILE if no arg specified: automagically in parse_hn_port()
+ # following does the same but additionally we can specify a log location
+ --logfile|--logfile=*|-oL|-oL=*)
+ "$do_logging" && fatal "two --log* arguments" $ERR_CMDLINE
+ LOGFILE="$(parse_opt_equal_sign "$1" "$2")"
+ [[ $? -eq 0 ]] && shift
+ do_logging=true
+ ;;
+ --json)
+ "$do_pretty_json" && fatal "flat and pretty JSON output are mutually exclusive" $ERR_CMDLINE
+ "$do_json" && fatal "--json and --jsonfile are mutually exclusive" $ERR_CMDLINE
+ if [[ "$2" =~ \.(json|JSON)$ ]]; then
+ fatal "No file name allowed after \"--json\" (use \"--jsonfile\" instead)" $ERR_CMDLINE
+ fi
+ do_json=true
+ ;; # DEFINITION of JSONFILE is not arg specified: automagically in parse_hn_port()
+ # following does the same but additionally we can specify a log location
+ --jsonfile|--jsonfile=*|-oj|-oj=*)
+ "$do_pretty_json" && fatal "flat and pretty JSON output are mutually exclusive" $ERR_CMDLINE
+ "$do_json" && fatal "--json and --jsonfile are mutually exclusive" $ERR_CMDLINE
+ JSONFILE="$(parse_opt_equal_sign "$1" "$2")"
+ [[ $? -eq 0 ]] && shift
+ do_json=true
+ ;;
+ --json-pretty)
+ "$do_json" && fatal "flat and pretty JSON output are mutually exclusive" $ERR_CMDLINE
+ "$do_pretty_json" && fatal "--json-pretty and --jsonfile-pretty are mutually exclusive" $ERR_CMDLINE
+ if [[ "$2" =~ \.(json|JSON)$ ]]; then
+ fatal "No file name allowed after \"--json\" (use \"--jsonfile-pretty\" instead)" $ERR_CMDLINE
+ fi
+ do_pretty_json=true
+ ;;
+ --jsonfile-pretty|--jsonfile-pretty=*|-oJ|-oJ=*)
+ "$do_json" && fatal "flat and pretty JSON output are mutually exclusive" $ERR_CMDLINE
+ "$do_pretty_json" && fatal "--json-pretty and --jsonfile-pretty are mutually exclusive" $ERR_CMDLINE
+ JSONFILE="$(parse_opt_equal_sign "$1" "$2")"
+ [[ $? -eq 0 ]] && shift
+ do_pretty_json=true
+ ;;
+ --severity|--severity=*)
+ set_severity_level "$(parse_opt_equal_sign "$1" "$2")"
+ [[ $? -eq 0 ]] && shift
+ ;;
+ --hints)
+ GIVE_HINTS=true
+ ;;
+ --csv)
+ "$do_csv" && fatal "two --csv* arguments" $ERR_CMDLINE
+ if [[ "$2" =~ \.(csv|CSV)$ ]]; then
+ fatal "No file name allowed after \"--csv\" (use \"--csvfile\" instead)" $ERR_CMDLINE
+ fi
+ do_csv=true
+ ;; # DEFINITION of CSVFILE is not arg specified: automagically in parse_hn_port()
+ # following does the same but additionally we can specify a log location
+ --csvfile|--csvfile=*|-oC|-oC=*)
+ "$do_csv" && fatal "two --csv* arguments" $ERR_CMDLINE
+ CSVFILE="$(parse_opt_equal_sign "$1" "$2")"
+ [[ $? -eq 0 ]] && shift
+ do_csv=true
+ ;;
+ --html)
+ "$do_html" && fatal "two --html* arguments" $ERR_CMDLINE
+ if [[ "$2" =~ \.(htm|html|HTM|HTML)$ ]]; then
+ fatal "No file name allowed after \"--html\" (use \"--htmlfile\" instead)" $ERR_CMDLINE
+ fi
+ do_html=true
+ ;; # DEFINITION of HTMLFILE is not arg specified: automagically in parse_hn_port()
+ # following does the same but additionally we can specify a file location
+ --htmlfile|--htmlfile=*|-oH|-oH=*)
+ "$do_html" && fatal "two --html* arguments" $ERR_CMDLINE
+ HTMLFILE="$(parse_opt_equal_sign "$1" "$2")"
+ [[ $? -eq 0 ]] && shift
+ do_html=true
+ ;;
+ --outfile|--outfile=*|-oa|-oa=*)
+ ( "$do_html" || "$do_json" || "$do_pretty_json" || "$do_csv" || "$do_logging" ) && fatal "check your arguments four multiple file output options" $ERR_CMDLINE
+ outfile_arg="$(parse_opt_equal_sign "$1" "$2")"
+ if [[ "$outfile_arg" != "auto" ]]; then
+ if [[ -d "$outfile_arg" ]]; then
+ HTMLFILE="$outfile_arg"
+ CSVFILE="$outfile_arg"
+ JSONFILE="$outfile_arg"
+ LOGFILE="$outfile_arg"
+ else
+ HTMLFILE="$outfile_arg.html"
+ CSVFILE="$outfile_arg.csv"
+ JSONFILE="$outfile_arg.json"
+ LOGFILE="$outfile_arg.log"
+ fi
+ fi
+ [[ $? -eq 0 ]] && shift
+ do_html=true
+ do_json=true
+ do_csv=true
+ do_logging=true
+ ;;
+ --outFile|--outFile=*|-oA|-oA=*)
+ ( "$do_html" || "$do_json" || "$do_pretty_json" || "$do_csv" || "$do_logging" ) && fatal "check your arguments four multiple file output options" $ERR_CMDLINE
+ outfile_arg="$(parse_opt_equal_sign "$1" "$2")"
+ if [[ "$outfile_arg" != "auto" ]]; then
+ if [[ -d "$outfile_arg" ]]; then
+ HTMLFILE="$outfile_arg"
+ CSVFILE="$outfile_arg"
+ JSONFILE="$outfile_arg"
+ LOGFILE="$outfile_arg"
+ else
+ HTMLFILE="$outfile_arg.html"
+ CSVFILE="$outfile_arg.csv"
+ JSONFILE="$outfile_arg.json"
+ LOGFILE="$outfile_arg.log"
+ fi
+ fi
+ [[ $? -eq 0 ]] && shift
+ do_html=true
+ do_pretty_json=true
+ do_csv=true
+ do_logging=true
+ ;;
+ --append)
+ APPEND=true
+ ;;
+ --outprefix)
+ FNAME_PREFIX="$(parse_opt_equal_sign "$1" "$2")"
+ if [[ $? -eq 0 ]]; then
+ shift
+ case "$(get_last_char "$FNAME_PREFIX")" in
+ '.') ;;
+ '-') ;;
+ '_') ;;
+ *) FNAME_PREFIX="${FNAME_PREFIX}-" ;;
+ esac
+ fi
+ ;;
+ --openssl|--openssl=*)
+ OPENSSL="$(parse_opt_equal_sign "$1" "$2")"
+ [[ $? -eq 0 ]] && shift
+ ;;
+ --openssl-timeout|--openssl-timeout=*)
+ OPENSSL_TIMEOUT="$(parse_opt_equal_sign "$1" "$2")"
+ [[ $? -eq 0 ]] && shift
+ ;;
+ --connect-timeout|--connect-timeout=*)
+ CONNECT_TIMEOUT="$(parse_opt_equal_sign "$1" "$2")"
+ [[ $? -eq 0 ]] && shift
+ ;;
+ --mapping|--mapping=*)
+ cipher_mapping="$(parse_opt_equal_sign "$1" "$2")"
+ [[ $? -eq 0 ]] && shift
+ case "$cipher_mapping" in
+ no-openssl) DISPLAY_CIPHERNAMES="rfc-only" ;;
+ no-rfc|no-iana) DISPLAY_CIPHERNAMES="openssl-only" ;;
+ openssl) DISPLAY_CIPHERNAMES="openssl" ;;
+ rfc|iana) DISPLAY_CIPHERNAMES="rfc" ;;
+ *) tmln_warning "\nmapping can only be \"no-openssl\", \"no-iana\"(\"no-rfc\"), \"openssl\" or \"iana\"(\"rfc\")" 1>&2
+ help 1 ;;
+ esac
+ ;;
+ --proxy|--proxy=*)
+ PROXY="$(parse_opt_equal_sign "$1" "$2")"
+ [[ $? -eq 0 ]] && shift
+ ;;
+ --phone-out)
+ PHONE_OUT=true
+ ;;
+ -6) # doesn't work automagically. My versions have -DOPENSSL_USE_IPV6, CentOS/RHEL/FC do not
+ HAS_IPv6=true
+ ;;
+ --has[-_]dhbits|--has[_-]dh[-_]bits)
+ # Should work automagically. Helper switch for CentOS,RHEL+FC w openssl server temp key backport (version 1.0.1), see #190
+ HAS_DH_BITS=true
+ ;;
+ --ssl_native|--ssl-native)
+ SSL_NATIVE=true
+ ;;
+ --basicauth|--basicauth=*)
+ BASICAUTH="$(parse_opt_equal_sign "$1" "$2")"
+ [[ $? -eq 0 ]] && shift
+ ;;
+ (--) shift
+ break
+ ;;
+ (-*) tmln_warning "$0: unrecognized option \"$1\"" 1>&2;
+ help 1
+ ;;
+ (*) break
+ ;;
+ esac
+ shift
+ done
+
+ # Show usage if no further options were specified
+ if [[ -z "$1" ]] && [[ -z "$FNAME" ]] && ! "$do_display_only"; then
+ fatal "URI missing" $ERR_CMDLINE
+ else
+ # What is left here should be the URI.
+ URI="$1"
+ [[ -n "$2" ]] && fatal "URI comes last" $ERR_CMDLINE
+ fi
+
+ # Now spot some incompatibilities in cmdlines
+ [[ $CMDLINE_IP == one ]] && [[ "$NODNS" == none ]] && fatal "\"--ip=one\" and \"--nodns=none\" don't work together" $ERR_CMDLINE
+ [[ $CMDLINE_IP == one ]] && ( is_ipv4addr "$URI" || is_ipv6addr "$URI" ) && fatal "\"--ip=one\" plus supplying an IP address doesn't work" $ERR_CMDLINE
+ "$do_mx_all_ips" && [[ "$NODNS" == none ]] && fatal "\"--mx\" and \"--nodns=none\" don't work together" $ERR_CMDLINE
+ [[ -n "$CONNECT_TIMEOUT" ]] && [[ "$MASS_TESTING_MODE" == parallel ]] && fatal "Parallel mass scanning and specifying connect timeouts currently don't work together" $ERR_CMDLINE
+
+ ADDITIONAL_CA_FILES="${ADDITIONAL_CA_FILES//,/ }"
+ for fname in $ADDITIONAL_CA_FILES; do
+ [[ -s "$fname" ]] || fatal "CA file \"$fname\" does not exist" $ERR_RESOURCE
+ grep -q "BEGIN CERTIFICATE" "$fname" || fatal "\"$fname\" is not CA file in PEM format" $ERR_RESOURCE
+ done
+
+ [[ "$DEBUG" -ge 5 ]] && debug_globals
+
+ count_do_variables
+ [[ $? -eq 0 ]] && set_scanning_defaults
+ CMDLINE_PARSED=true
+}
+
+
+# connect call from openssl needs ipv6 in square brackets
+nodeip_to_proper_ip6() {
+ local len_nodeip=0
+
+ if is_ipv6addr $NODEIP; then
+ ${UNBRACKTD_IPV6} || NODEIP="[$NODEIP]"
+ len_nodeip=${#NODEIP}
+ CORRECT_SPACES="$(printf -- " "'%.s' $(eval "echo {1.."$((len_nodeip - 17))"}"))"
+ # IPv6 addresses are longer, this variable takes care that "further IP" and "Service" is properly aligned
+ fi
+}
+
+
+reset_hostdepended_vars() {
+ TLS_EXTENSIONS=""
+ PROTOS_OFFERED=""
+ CURVES_OFFERED=""
+ OPTIMAL_PROTO=""
+ ALL_FAILED_SOCKETS=true
+ SERVER_SIZE_LIMIT_BUG=false
+}
+
+# Rough estimate, in the future we maybe want to make use of nano secs (%N). Note this
+# is for performance debugging purposes (MEASURE_TIME=yes), eye candy is not important.
+#
+stopwatch() {
+ local new_delta
+ local column=$((COLUMNS - 0)) # for future adjustments
+
+ "$MEASURE_TIME" || return
+ new_delta=$(( $(date +%s) - LAST_TIME ))
+ printf "%${column}s" "$1: $new_delta"
+ [[ -e "$MEASURE_TIME_FILE" ]] && echo "$1 : $new_delta " >> "$MEASURE_TIME_FILE"
+ LAST_TIME=$(( new_delta + LAST_TIME ))
+}
+
+
+# arg1(optional): "init" --> just initializing. Or: STARTTLS protocol
+lets_roll() {
+ local -i ret=0
+ local section_number=0
+
+ if [[ "$1" == init ]]; then
+ # called once upfront to be able to measure preparation time b4 everything starts
+ START_TIME=$(date +%s)
+ LAST_TIME=$START_TIME
+ [[ -n "$MEASURE_TIME_FILE" ]] && >"$MEASURE_TIME_FILE"
+ return 0
+ fi
+ stopwatch initialized
+
+ [[ -z "$NODEIP" ]] && fatal "$NODE doesn't resolve to an IP address" $ERR_DNSLOOKUP
+ nodeip_to_proper_ip6
+ reset_hostdepended_vars
+ determine_rdns # Returns always zero or has already exited if fatal error occurred
+ stopwatch determine_rdns
+
+ ((SERVER_COUNTER++))
+ datebanner " Start"
+ determine_service "$1" # STARTTLS service? Other will be determined here too. Returns 0 if test connect was ok or has already exited if fatal error occurred
+ # determine_service() can return 1, it indicates that this IP cannot be reached but there are more IPs to check
+ if [[ $? -eq 0 ]] ; then
+ # "secret" devel options --devel:
+ if "$do_tls_sockets"; then
+ if [[ "$TLS_LOW_BYTE" == 22 ]]; then
+ sslv2_sockets "" "true"
+ else
+ if [[ "$TLS_LOW_BYTE" == 04 ]]; then
+ if "$CERT_COMPRESSION"; then
+ # See PR #1279
+ [[ $DEBUG -eq 3 ]] && tmln_out "including TLS extension certificate compression"
+ tls_sockets "$TLS_LOW_BYTE" "$HEX_CIPHER" "all+" "00,1b, 00,03, 02, 00,01"
+ else
+ tls_sockets "$TLS_LOW_BYTE" "$HEX_CIPHER" "ephemeralkey"
+ fi
+ else
+ tls_sockets "$TLS_LOW_BYTE" "$HEX_CIPHER" "all"
+ fi
+ fi
+ echo $?
+ exit $ALLOK;
+ fi
+ if "$do_cipher_match"; then
+ # we will have an invalid JSON with no if statement
+ fileout_section_header $section_number false
+ run_cipher_match ${single_cipher}
+ stopwatch run_cipher_match
+ else
+ fileout_section_header $section_number false && ((section_number++))
+ determine_sizelimitbug
+ fileout_section_footer false
+
+ ((section_number++))
+ # all top level functions now following have the prefix "run_"
+ fileout_section_header $section_number false && ((section_number++))
+ "$do_protocols" && {
+ run_protocols; ret=$(($? + ret)); stopwatch run_protocols;
+ run_npn; ret=$(($? + ret)); stopwatch run_npn;
+ run_alpn; ret=$(($? + ret)); stopwatch run_alpn;
+ }
+ fileout_section_header $section_number true && ((section_number++))
+ "$do_grease" && { run_grease; ret=$(($? + ret)); stopwatch run_grease; }
+
+ fileout_section_header $section_number true && ((section_number++))
+ "$do_cipherlists" && { run_cipherlists; ret=$(($? + ret)); stopwatch run_cipherlists; }
+
+ fileout_section_header $section_number true && ((section_number++))
+ "$do_pfs" && { run_pfs; ret=$(($? + ret)); stopwatch run_pfs; }
+
+ fileout_section_header $section_number true && ((section_number++))
+ "$do_server_preference" && { run_server_preference; ret=$(($? + ret)); stopwatch run_server_preference; }
+
+ fileout_section_header $section_number true && ((section_number++))
+ "$do_server_defaults" && { run_server_defaults; ret=$(($? + ret)); stopwatch run_server_defaults; }
+
+ if "$do_header"; then
+ #TODO: refactor this into functions
+ fileout_section_header $section_number true && ((section_number++))
+ if [[ $SERVICE == HTTP ]]; then
+ run_http_header "$URL_PATH"; ret=$(($? + ret))
+ run_http_date "$URL_PATH"; ret=$(($? + ret))
+ run_hsts "$URL_PATH"; ret=$(($? + ret))
+ run_hpkp "$URL_PATH"; ret=$(($? + ret))
+ run_server_banner "$URL_PATH"; ret=$(($? + ret))
+ run_appl_banner "$URL_PATH"; ret=$(($? + ret))
+ run_cookie_flags "$URL_PATH"; ret=$(($? + ret))
+ run_security_headers "$URL_PATH"; ret=$(($? + ret))
+ run_rp_banner "$URL_PATH"; ret=$(($? + ret))
+ stopwatch do_header
+ fi
+ else
+ ((section_number++))
+ fi
+
+ # vulnerabilities
+ if [[ $VULN_COUNT -gt $VULN_THRESHLD ]] || "$do_vulnerabilities"; then
+ outln; pr_headlineln " Testing vulnerabilities "
+ outln
+ fi
+
+ fileout_section_header $section_number true && ((section_number++))
+ "$do_heartbleed" && { run_heartbleed; ret=$(($? + ret)); stopwatch run_heartbleed; }
+ "$do_ccs_injection" && { run_ccs_injection; ret=$(($? + ret)); stopwatch run_ccs_injection; }
+ "$do_ticketbleed" && { run_ticketbleed; ret=$(($? + ret)); stopwatch run_ticketbleed; }
+ "$do_robot" && { run_robot; ret=$(($? + ret)); stopwatch run_robot; }
+ "$do_renego" && { run_renego; ret=$(($? + ret)); stopwatch run_renego; }
+ "$do_crime" && { run_crime; ret=$(($? + ret)); stopwatch run_crime; }
+ "$do_breach" && { run_breach "$URL_PATH" ; ret=$(($? + ret)); stopwatch run_breach; }
+ "$do_ssl_poodle" && { run_ssl_poodle; ret=$(($? + ret)); stopwatch run_ssl_poodle; }
+ "$do_tls_fallback_scsv" && { run_tls_fallback_scsv; ret=$(($? + ret)); stopwatch run_tls_fallback_scsv; }
+ "$do_sweet32" && { run_sweet32; ret=$(($? + ret)); stopwatch run_sweet32; }
+ "$do_freak" && { run_freak; ret=$(($? + ret)); stopwatch run_freak; }
+ "$do_drown" && { run_drown ret=$(($? + ret)); stopwatch run_drown; }
+ "$do_logjam" && { run_logjam; ret=$(($? + ret)); stopwatch run_logjam; }
+ "$do_beast" && { run_beast; ret=$(($? + ret)); stopwatch run_beast; }
+ "$do_lucky13" && { run_lucky13; ret=$(($? + ret)); stopwatch run_lucky13; }
+ "$do_rc4" && { run_rc4; ret=$(($? + ret)); stopwatch run_rc4; }
+
+ fileout_section_header $section_number true && ((section_number++))
+ "$do_allciphers" && { run_allciphers; ret=$(($? + ret)); stopwatch run_allciphers; }
+ "$do_cipher_per_proto" && { run_cipher_per_proto; ret=$(($? + ret)); stopwatch run_cipher_per_proto; }
+
+ fileout_section_header $section_number true && ((section_number++))
+ "$do_client_simulation" && { run_client_simulation; ret=$(($? + ret)); stopwatch run_client_simulation; }
+ fi
+ fileout_section_footer true
+ fi
+
+ outln
+ calc_scantime
+ datebanner " Done"
+
+ # reset the failed connect counter as we are finished
+ NR_SOCKET_FAIL=0
+ NR_OSSL_FAIL=0
+
+ "$MEASURE_TIME" && printf "$1: %${COLUMNS}s\n" "$SCAN_TIME"
+ [[ -e "$MEASURE_TIME_FILE" ]] && echo "Total : $SCAN_TIME " >> "$MEASURE_TIME_FILE"
+
+ return $ret
+}
+
+
+
+################# main #################
+
+
+ RET=0 # this is a global as we can have a function main(), see #705. Should we toss then all local $ret?
+ ip=""
+ stopwatch start
+
+ lets_roll init
+ initialize_globals
+ check_base_requirements # needs to come after $do_html is defined
+ parse_cmd_line "$@"
+ # CMDLINE_PARSED has been set now. Don't put a function immediately after this which calls fatal().
+ # Rather put it after csv_header below.
+ # html_header() needs to be called early! Otherwise if html_out() is called before html_header() and the
+ # command line contains --htmlfile <htmlfile> or --html, it'll make problems with html output, see #692.
+ # json_header and csv_header could be called later but for context reasons we'll leave it here
+ html_header
+ json_header
+ csv_header
+ get_install_dir
+ # see #705, we need to source TLS_DATA_FILE here instead of in get_install_dir(), see #705
+ [[ -r "$TLS_DATA_FILE" ]] && . "$TLS_DATA_FILE"
+ set_color_functions
+ maketempf
+ find_openssl_binary
+ choose_printf
+ check_resolver_bins
+ prepare_debug ; stopwatch parse
+ prepare_arrays ; stopwatch prepare_arrays
+ mybanner
+ check_proxy
+ check4openssl_oldfarts
+ check_bsd_mount
+
+
+ if "$do_display_only"; then
+ prettyprint_local "$PATTERN2SHOW"
+ exit $?
+ fi
+ fileout_banner
+
+ if "$do_mass_testing"; then
+ prepare_logging
+ if [[ "$MASS_TESTING_MODE" == parallel ]]; then
+ run_mass_testing_parallel
+ else
+ run_mass_testing
+ fi
+ exit $?
+ fi
+ html_banner
+
+ #TODO: there shouldn't be the need for a special case for --mx, only the ip addresses we would need upfront and the do-parser
+ if "$do_mx_all_ips"; then
+ #FIXME: do we need this really here?
+ count_do_variables # if we have just 1x "do_*" --> we do a standard run -- otherwise just the one specified
+ [[ $? -eq 1 ]] && set_scanning_defaults
+ run_mx_all_ips "${URI}" $PORT # we should reduce run_mx_all_ips to what's necessary as below we have similar code
+ exit $?
+ fi
+
+ [[ -z "$NODE" ]] && parse_hn_port "${URI}" # NODE, URL_PATH, PORT, IPADDRs and IP46ADDR is set now
+ prepare_logging
+
+ if ! determine_ip_addresses; then
+ fatal "No IP address could be determined" $ERR_DNSLOOKUP
+ fi
+ if [[ $(count_words "$IPADDRs") -gt 1 ]]; then # we have more than one ipv4 address to check
+ MULTIPLE_CHECKS=true
+ pr_bold "Testing all IPv4 addresses (port $PORT): "; outln "$IPADDRs"
+ for ip in $IPADDRs; do
+ draw_line "-" $((TERM_WIDTH * 2 / 3))
+ outln
+ NODEIP="$ip"
+ lets_roll "${STARTTLS_PROTOCOL}"
+ RET=$((RET + $?)) # RET value per IP address
+ done
+ draw_line "-" $((TERM_WIDTH * 2 / 3))
+ outln
+ pr_bold "Done testing now all IP addresses (on port $PORT): "; outln "$IPADDRs"
+ else # Just 1x ip4v to check, applies also if CMDLINE_IP was supplied
+ NODEIP="$IPADDRs"
+ lets_roll "${STARTTLS_PROTOCOL}"
+ RET=$?
+ fi
+
+exit $RET
+