From 825cbf7b1deafe89f6a05e80bad5b2d88db3bf0a Mon Sep 17 00:00:00 2001 From: Dirk Wetter Date: Mon, 11 May 2020 10:41:48 +0200 Subject: [PATCH 1/8] Improve compression detection for BREACH This commit tries to enummerate through all possible compressions instead of just raising the arm because of the first one detected. As far as the performance is concerned there's room for improvements which subsequent commits will address. --- testssl.sh | 67 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 46 insertions(+), 21 deletions(-) diff --git a/testssl.sh b/testssl.sh index 39c67e3..6c61516 100755 --- a/testssl.sh +++ b/testssl.sh @@ -15887,6 +15887,7 @@ run_crime() { # 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 +# Any URL can be vulnerable. Here only the given URL is tested. See also $when_makesense # run_breach() { local header @@ -15899,8 +15900,10 @@ run_breach() { 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 hint="" c="" local jsonID="BREACH" + local compressions="gzip deflate compress br" + local has_compression=() [[ $SERVICE != HTTP ]] && ! "$CLIENT_AUTH" && return 7 @@ -15925,33 +15928,55 @@ run_breach() { [[ "$NODE" =~ google ]] && referer="https://yandex.ru/" # otherwise we have a false positive for google.com useragent="$UA_STD" $SNEAKY && useragent="$UA_SNEAKY" - tm_out "GET $url HTTP/1.1\r\nHost: $NODE\r\nUser-Agent: $useragent\r\nReferer: $referer\r\nConnection: Close\r\nAccept-encoding: gzip,deflate,compress\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=$(awk '/^Content-Encoding/ { print $2 }' $TMPFILE) - result=$(strip_lf "$result") - debugme grep '^Content-Encoding' $TMPFILE - 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" + for c in $compressions; do + tm_out "GET $url HTTP/1.1\r\nHost: $NODE\r\nUser-Agent: $useragent\r\nReferer: $referer\r\nConnection: Close\r\nAccept-encoding: $c\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) + debugme echo "$result" + result="$(strip_lf "$result")" + result="${result#*:}" + result="$(strip_spaces "$result")" + if [[ ! -s $TMPFILE ]]; then + if [[ $was_killed -ne 0 ]]; then + has_compression+=("$c:warn_stalled") + else + has_compression+=("$c:warn_empty") + fi + ret+=1 + elif [[ -z $result ]]; then + has_compression+=("$c:no") else - fileout "$jsonID" "WARN" "Test failed as HTTP response was empty" "$cve" "$cwe" + has_compression+=("$c:yes") fi - prln_warning ") " - ret=1 - elif [[ -z $result ]]; then - pr_svrty_best "no HTTP compression (OK) " + done + + result="" + if [[ ! ${has_compression[@]} =~ yes ]] && [[ ! ${has_compression[@]} =~ warn ]]; then + pr_svrty_best "no gzip/deflate/compress/br HTTP compression (OK) " outln "$disclaimer" - fileout "$jsonID" "OK" "not vulnerable, no HTTP compression $disclaimer" "$cve" "$cwe" + fileout "$jsonID" "OK" "not vulnerable, no gzip/deflate/compress/br HTTP compression $disclaimer" "$cve" "$cwe" + elif [[ ${has_compression[@]} =~ warn_stalled ]]; then + pr_warning "At least 1/4 checks failed (HTTP header request stalled and was terminated" + out ", debug: ${has_compression[@]})" + fileout "$jsonID" "WARN" "Test failed as HTTP request stalled and was terminated" "$cve" "$cwe" + elif [[ ${has_compression[@]} =~ warn_empty ]]; then + pr_warning "At least 1/4 checks failed (HTTP header request was empty, debug: ${has_compression[@]}" + out ", debug: ${has_compression[@]})" + fileout "$jsonID" "WARN" "Test failed as HTTP response was empty, debug: ${has_compression[@]}" "$cve" "$cwe" else - pr_svrty_high "potentially NOT ok, uses $result HTTP compression." + for c in ${has_compression[@]}; do + if [[ $c =~ yes ]]; then + result+="${c%:*} " + fi + done + result="$(strip_trailing_space "$result")" + pr_svrty_high "potentially NOT ok, \"$result\" HTTP compression detected." outln "$disclaimer" outln "$spaces$when_makesense" - fileout "$jsonID" "HIGH" "potentially VULNERABLE, uses $result HTTP compression $disclaimer" "$cve" "$cwe" "$hint" + fileout "$jsonID" "HIGH" "potentially VULNERABLE, $result HTTP compression detected $disclaimer" "$cve" "$cwe" "$hint" fi - # Any URL can be vulnerable. I am testing now only the given URL! + debugme echo "has_compression: ${has_compression[@]}" tmpfile_handle ${FUNCNAME[0]}.txt return $ret From 25f87455a4bc4ce442396b6bdd2afd005dede07b Mon Sep 17 00:00:00 2001 From: Dirk Wetter Date: Mon, 11 May 2020 13:57:50 +0200 Subject: [PATCH 2/8] Amendment for compression detection (BREACH) This commit saves more or less time for a detection of the compression. First it assembles the GET command with all available compressions and send them all. If the result is negative: we can just tell the finding and return. If it's positive: We already have identified 1x compression. Then we cycle through the remaining compressions with single GET requests. In order to not duplicate code we introduced a helper function sub_breach_helper() which takes care sending the request and analysis the result. We treat now failed requests differently: When the first fails we don't continue anymore. --- testssl.sh | 167 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 110 insertions(+), 57 deletions(-) diff --git a/testssl.sh b/testssl.sh index 6c61516..13c633d 100755 --- a/testssl.sh +++ b/testssl.sh @@ -15884,15 +15884,48 @@ run_crime() { } -# 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! + +# As the name says. It expects as arg1 a GET command string. It returns 1 +# when GET command was stalled or killed (which is no not always used) +# and echos "warn_*". It return 0 when everything went ok and echos the +# compression if any. +sub_breach_helper() { + local get_command="$1" + local detected_compression="" + local -i was_killed=0 + + safe_echo "$get_command" | $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 when it was killed + detected_compression=$(grep -ia ^Content-Encoding: $TMPFILE) + detected_compression="$(strip_lf "$detected_compression")" + detected_compression="${detected_compression#*:}" + detected_compression="$(strip_spaces "$detected_compression")" + if [[ ! -s $TMPFILE ]]; then + if [[ $was_killed -eq 0 ]]; then + echo "warn_stalled" + else + echo "warn_killed" + fi + return 1 + elif [[ -z $detected_compression ]]; then + echo "no_compression" + else + echo "$detected_compression" + fi + return 0 +} + + + +# 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/ . External referrers are the important thing here! # Mitigation: see https://community.qualys.com/message/20360 # Any URL can be vulnerable. Here only the given URL is tested. See also $when_makesense # run_breach() { local header local -i ret=0 - local -i was_killed=0 local referer useragent local url="$1" local spaces=" " @@ -15904,6 +15937,8 @@ run_breach() { local jsonID="BREACH" local compressions="gzip deflate compress br" local has_compression=() + local detected_compression="" + local get_command="" [[ $SERVICE != HTTP ]] && ! "$CLIENT_AUTH" && return 7 @@ -15914,13 +15949,6 @@ run_breach() { fileout "$jsonID" "INFO" "was not tested, server side requires x509 authentication" "$cve" "$cwe" 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" @@ -15928,55 +15956,80 @@ run_breach() { [[ "$NODE" =~ google ]] && referer="https://yandex.ru/" # otherwise we have a false positive for google.com useragent="$UA_STD" $SNEAKY && useragent="$UA_SNEAKY" - for c in $compressions; do - tm_out "GET $url HTTP/1.1\r\nHost: $NODE\r\nUser-Agent: $useragent\r\nReferer: $referer\r\nConnection: Close\r\nAccept-encoding: $c\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) - debugme echo "$result" - result="$(strip_lf "$result")" - result="${result#*:}" - result="$(strip_spaces "$result")" - if [[ ! -s $TMPFILE ]]; then - if [[ $was_killed -ne 0 ]]; then - has_compression+=("$c:warn_stalled") - else - has_compression+=("$c:warn_empty") - fi - ret+=1 - elif [[ -z $result ]]; then - has_compression+=("$c:no") - else - has_compression+=("$c:yes") - fi - done - result="" - if [[ ! ${has_compression[@]} =~ yes ]] && [[ ! ${has_compression[@]} =~ warn ]]; then - pr_svrty_best "no gzip/deflate/compress/br HTTP compression (OK) " - outln "$disclaimer" - fileout "$jsonID" "OK" "not vulnerable, no gzip/deflate/compress/br HTTP compression $disclaimer" "$cve" "$cwe" - elif [[ ${has_compression[@]} =~ warn_stalled ]]; then - pr_warning "At least 1/4 checks failed (HTTP header request stalled and was terminated" - out ", debug: ${has_compression[@]})" - fileout "$jsonID" "WARN" "Test failed as HTTP request stalled and was terminated" "$cve" "$cwe" - elif [[ ${has_compression[@]} =~ warn_empty ]]; then - pr_warning "At least 1/4 checks failed (HTTP header request was empty, debug: ${has_compression[@]}" - out ", debug: ${has_compression[@]})" - fileout "$jsonID" "WARN" "Test failed as HTTP response was empty, debug: ${has_compression[@]}" "$cve" "$cwe" - else - for c in ${has_compression[@]}; do - if [[ $c =~ yes ]]; then - result+="${c%:*} " + # Assemble the GET command with all available compressions and send them all, initially. + # If the result is negative: we can just tell the finding and return. If it's + # positive: We already have identified 1x compression + get_command="GET $url HTTP/1.1\r\nHost: $NODE\r\nUser-Agent: $useragent\r\nReferer: $referer\r\nConnection: Close\r\nAccept-encoding: ${compressions// /,}\r\nAccept: text/*\r\n\r\n" + detected_compression=$(sub_breach_helper "$get_command") + case "$detected_compression" in + warn_stalled) + pr_warning "First request failed (HTTP header request stalled and was terminated)" + fileout "$jsonID" "WARN" "Test failed as first HTTP request stalled and was terminated" "$cve" "$cwe" + ret=1 + ;; + warn_failed) + pr_warning "First request failed (HTTP header request was empty)" + fileout "$jsonID" "WARN" "Test failed as first HTTP response was empty" "$cve" "$cwe" + ret=1 + ;; + no_compression) + pr_svrty_best "no gzip/deflate/compress/br HTTP compression (OK) " + outln "$disclaimer" + fileout "$jsonID" "OK" "not vulnerable, no gzip/deflate/compress/br HTTP compression $disclaimer" "$cve" "$cwe" + ret=0 + ;; + *) # Now assemble the remaining compressions in $compressions and loop through them + has_compression+=("$detected_compression:yes") + compressions="${compressions//$detected_compression/}" + for c in $compressions; do + get_command="GET $url HTTP/1.1\r\nHost: $NODE\r\nUser-Agent: $useragent\r\nReferer: $referer\r\nConnection: Close\r\nAccept-encoding: ${c}\r\nAccept: text/*\r\n\r\n" + detected_compression=$(sub_breach_helper "$get_command") + if [[ $? -ne 0 ]]; then + # This failure unlikely here. The initial request must have succeeded and this one then + # failed but we'd rather treat this correctly (e.d. IDS which triggers later). Not also + # we exit on the first stalled request. So if the first one with all compressions failed, + # we don't get here. It seems very unlikely the first failed and subsequent will succeed. + has_compression+=("$c:$compressions") + elif [[ "$detected_compression" =~ no_compression ]]; then + has_compression+=("$c:no") + debugme echo "has_compression: $c: no" + elif [[ -n "detected_compression" ]]; then + has_compression+=("$c:yes") + debugme echo "has_compression: $c: yes" + else + prln_fixme "strange reply around line $((LINENO)) from sub_breach_helper()" + fi + done + + # Final verdict (if not happened preemptively before). We reuse $detected_compression here + detected_compression="" + if [[ ${has_compression[@]} =~ warn ]]; then + # warn_empty / warn_stalled + if [[ ${has_compression[@]} =~ warn_empty ]]; then + pr_warning "At least 1/4 checks failed (HTTP header request was empty, debug: ${has_compression[@]}" + out ", debug: ${has_compression[@]})" + fileout "$jsonID" "WARN" "Test failed as HTTP response was empty, debug: ${has_compression[@]}" "$cve" "$cwe" + else # warn_stalled + pr_warning "At least 1/4 checks failed (HTTP header request stalled and was terminated" + out ", debug: ${has_compression[@]})" + fileout "$jsonID" "WARN" "Test failed as HTTP request stalled and was terminated" "$cve" "$cwe" + fi + else + for c in ${has_compression[@]}; do + if [[ $c =~ yes ]]; then + detected_compression+="${c%:*} " + fi + done + detected_compression="$(strip_trailing_space "$detected_compression")" + pr_svrty_high "potentially NOT ok, \"$detected_compression\" HTTP compression detected." + outln "$disclaimer" + outln "${spaces}${when_makesense}" + fileout "$jsonID" "HIGH" "potentially VULNERABLE, $detected_compression HTTP compression detected $disclaimer" "$cve" "$cwe" "$hint" fi - done - result="$(strip_trailing_space "$result")" - pr_svrty_high "potentially NOT ok, \"$result\" HTTP compression detected." - outln "$disclaimer" - outln "$spaces$when_makesense" - fileout "$jsonID" "HIGH" "potentially VULNERABLE, $result HTTP compression detected $disclaimer" "$cve" "$cwe" "$hint" - fi - debugme echo "has_compression: ${has_compression[@]}" + debugme outln "${spaces}has_compression: ${has_compression[@]}" + ;; + esac tmpfile_handle ${FUNCNAME[0]}.txt return $ret From 78a95d73c8608cd2db559d45955b5e9b912e4c70 Mon Sep 17 00:00:00 2001 From: Dirk Wetter Date: Mon, 11 May 2020 14:07:02 +0200 Subject: [PATCH 3/8] Add BREACH extension --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36d0236..6641957 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ * Added several ciphers to colored ciphers * Percent output char problem fixed * Several display/output fixes +* BREACH check: list all compression methods and add brotli * Security fix: DNS input * Don't use external pwd anymore * STARTTLS: XMPP server support From d5671a0352d15ed20e09dcba52fcdc75f44d6014 Mon Sep 17 00:00:00 2001 From: David Cooper Date: Mon, 11 May 2020 10:09:02 -0400 Subject: [PATCH 4/8] Fix $SHOW_SIGALGO This commit fixes two issues related to $SHOW_SIGALGO. First, cipher_pref_check() does not show the signature algorithm if any of the ciphers were found using tls_sockets(), since the call to tls_sockets() does not specify that the server's certificate should be extracted. Second, in run_beast() the call to tls_sockets() indicates that the server's certificate should be extracted if "$SHOW_SIGALGO" is true, even if "$WIDE" is false. While this does not cause any problems, extracting the certificate is a waste of effort if "$WIDE" is false, since the signature algorithm is not shown in that case. --- testssl.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/testssl.sh b/testssl.sh index 39c67e3..3570521 100755 --- a/testssl.sh +++ b/testssl.sh @@ -6993,7 +6993,11 @@ cipher_pref_check() { ! "${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" + if "$wide" && "$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 cipher=$(get_cipher "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt") for (( i=0; i < nr_ciphers; i++ )); do @@ -17006,7 +17010,7 @@ run_beast(){ ! "${ciphers_found[i]}" && ciphers_to_test+=", ${hexcode[i]}" done [[ -z "$ciphers_to_test" ]] && break - if "$SHOW_SIGALGO"; then + if "$WIDE" && "$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" From df42eeb8b4b1510a8550b55702f25ef4d1af6ff6 Mon Sep 17 00:00:00 2001 From: David Cooper Date: Thu, 14 May 2020 14:42:08 -0400 Subject: [PATCH 5/8] Extract server's signature algorithm PR #1519 requested that testssl.sh show the signature algorithm that the server uses during the TLS handshake. In TLS 1.3, this appears in the CertificateVerify message. In TLS 1.2 it appears in the ServerKeyExchange message when the chosen cipher suite uses an ephemeral (DH or ECDH) key, except in the case of cipher suites that provide no authentication. This information is not present in TLS 1.1 and earlier, as the hash algorithm to use in these earlier versions of the protocol is hard coded into the specification. This commit takes a first step towards being able to show the signature algorithm by extending parse_tls_serverhello() to extract the signature algorithm when it is present. Matching the output produced by OpenSSL, it output two separate lines, the "Peer signature type" (RSA, RSA-PSS, DSA, ECDSA, Ed25519, or Ed448) and the "Peer signing digest" (MD5, SHA1, SHA224, SHA256, SHA384, or SHA512). This will allow the same function to extract the signature algorithm and digest, whether the handshake was performed using "$OPENSSL s_client" or tls_sockets(). --- testssl.sh | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 87 insertions(+), 3 deletions(-) diff --git a/testssl.sh b/testssl.sh index ebf8389..e29bda3 100755 --- a/testssl.sh +++ b/testssl.sh @@ -12800,6 +12800,7 @@ parse_tls_serverhello() { 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 + local peering_signing_digest=0 peer_signature_type=0 DETECTED_TLS_VERSION="" [[ $DEBUG -ge 1 ]] && echo > $TMPFILE @@ -13033,6 +13034,14 @@ parse_tls_serverhello() { fi tls_serverkeyexchange_ascii="${tls_handshake_ascii:i:msg_len}" tls_serverkeyexchange_ascii_len=$msg_len + elif [[ "$tls_msg_type" == 0F ]]; then + if [[ $msg_len -lt 4 ]]; then + debugme tmln_warning "Response contained malformed certificate_verify message." + return 1 + fi + # Extract just the SignatureAndHashAlgorithm from the CertificateVerify message. + peering_signing_digest="${tls_handshake_ascii:i:2}" + peer_signature_type="${tls_handshake_ascii:$((i+2)):2}" 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." @@ -13811,12 +13820,24 @@ parse_tls_serverhello() { 29) dh_bits=253 ; named_curve_str="X25519" ;; 30) dh_bits=448 ; named_curve_str="X448" ;; esac + if [[ "$DETECTED_TLS_VERSION" == 0303 ]]; then + # Skip over the public key to get to the SignatureAndHashAlgorithm + # This is TLS 1.2-only, as this field does not appear in earlier versions. + len1=2*$(hex2dec "${tls_serverkeyexchange_ascii:6:2}") + offset=$((len1+8)) + if [[ $tls_serverkeyexchange_ascii_len -ge $((offset+4)) ]]; then + # The SignatureAndHashAlgorithm won't be present in an anonymous + # key exhange. + peering_signing_digest="${tls_serverkeyexchange_ascii:offset:2}" + peer_signature_type="${tls_serverkeyexchange_ascii:$((offset+2)):2}" + fi + fi 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" + [[ $DEBUG -ge 3 ]] && echo -e " dh_bits: ECDH, $named_curve_str, $dh_bits bits" 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" + [[ $DEBUG -ge 3 ]] && echo -e " dh_bits: $named_curve_str, $dh_bits bits" echo "Server Temp Key: $named_curve_str, $dh_bits bits" >> $TMPFILE fi elif [[ $rfc_cipher_suite =~ TLS_DHE_ ]] || [[ $rfc_cipher_suite =~ TLS_DH_anon ]] || \ @@ -13875,10 +13896,73 @@ parse_tls_serverhello() { [[ "$ephemeral_param" != "$rfc7919_param" ]] && named_curve_str="" fi - [[ $DEBUG -ge 3 ]] && [[ $dh_bits -ne 0 ]] && echo -e " dh_bits: DH,$named_curve_str $dh_bits bits\n" + [[ $DEBUG -ge 3 ]] && [[ $dh_bits -ne 0 ]] && echo -e " dh_bits: DH,$named_curve_str $dh_bits bits" [[ $dh_bits -ne 0 ]] && echo "Server Temp Key: DH,$named_curve_str $dh_bits bits" >> $TMPFILE + if [[ "$DETECTED_TLS_VERSION" == 0303 ]]; then + # Skip over the public key (P, G, Y) to get to the SignatureAndHashAlgorithm + # This is TLS 1.2-only, as this field does not appear in earlier versions. + offset=$((dh_p_len+4)) + if [[ $tls_serverkeyexchange_ascii_len -lt $((offset+4)) ]]; then + debugme echo "Malformed ServerKeyExchange Handshake message in ServerHello." + tmpfile_handle ${FUNCNAME[0]}.txt + return 1 + fi + len1=2*$(hex2dec "${tls_serverkeyexchange_ascii:offset:4}") + offset+=$((len1+4)) + if [[ $tls_serverkeyexchange_ascii_len -lt $((offset+4)) ]]; then + debugme echo "Malformed ServerKeyExchange Handshake message in ServerHello." + tmpfile_handle ${FUNCNAME[0]}.txt + return 1 + fi + len1=2*$(hex2dec "${tls_serverkeyexchange_ascii:offset:4}") + offset+=$((len1+4)) + if [[ $tls_serverkeyexchange_ascii_len -ge $((offset+4)) ]]; then + # The SignatureAndHashAlgorithm won't be present in an anonymous + # key exhange. + peering_signing_digest="${tls_serverkeyexchange_ascii:offset:2}" + peer_signature_type="${tls_serverkeyexchange_ascii:$((offset+2)):2}" + fi + fi fi fi + if [[ 0x$peering_signing_digest -eq 8 ]] && \ + [[ 0x$peer_signature_type -ge 4 ]] && [[ 0x$peer_signature_type -le 11 ]]; then + case $peer_signature_type in + 04) peering_signing_digest="SHA256"; peer_signature_type="RSA-PSS" ;; + 05) peering_signing_digest="SHA384"; peer_signature_type="RSA-PSS" ;; + 06) peering_signing_digest="SHA512"; peer_signature_type="RSA-PSS" ;; + 07) peering_signing_digest=""; peer_signature_type="Ed25519" ;; + 08) peering_signing_digest=""; peer_signature_type="Ed448" ;; + 09) peering_signing_digest="SHA256"; peer_signature_type="RSA-PSS" ;; + 0A) peering_signing_digest="SHA384"; peer_signature_type="RSA-PSS" ;; + 0B) peering_signing_digest="SHA512"; peer_signature_type="RSA-PSS" ;; + esac + if [[ -n "$peering_signing_digest" ]]; then + echo "Peer signing digest: $peering_signing_digest" >> $TMPFILE + [[ $DEBUG -ge 3 ]] && echo -e " Peer signing digest: $peering_signing_digest" + fi + echo "Peer signature type: $peer_signature_type" >> $TMPFILE + [[ $DEBUG -ge 3 ]] && echo -e " Peer signature type: $peer_signature_type\n" + elif [[ 0x$peering_signing_digest -ge 1 ]] && [[ 0x$peering_signing_digest -le 6 ]] && \ + [[ 0x$peer_signature_type -ge 1 ]] && [[ 0x$peer_signature_type -le 3 ]]; then + case $peering_signing_digest in + 01) peering_signing_digest="MD5" ;; + 02) peering_signing_digest="SHA1" ;; + 03) peering_signing_digest="SHA224" ;; + 04) peering_signing_digest="SHA256" ;; + 05) peering_signing_digest="SHA384" ;; + 06) peering_signing_digest="SHA512" ;; + esac + case $peer_signature_type in + 01) peer_signature_type="RSA" ;; + 02) peer_signature_type="DSA" ;; + 03) peer_signature_type="ECDSA" ;; + esac + echo "Peer signing digest: $peering_signing_digest" >> $TMPFILE + [[ $DEBUG -ge 3 ]] && echo -e " Peer signing digest: $peering_signing_digest" + echo "Peer signature type: $peer_signature_type" >> $TMPFILE + [[ $DEBUG -ge 3 ]] && echo -e " Peer signature type: $peer_signature_type\n" + fi tmpfile_handle ${FUNCNAME[0]}.txt TLS_SERVER_HELLO="02$(printf "%06x" $(( tls_serverhello_ascii_len/2)) )${tls_serverhello_ascii}" From 3ae48931fb0aebc9132dd3fc88d142b09a84b7f0 Mon Sep 17 00:00:00 2001 From: David Cooper Date: Thu, 14 May 2020 14:55:48 -0400 Subject: [PATCH 6/8] Support EdDSA This commit adds support for EdDSA (Ed25519 and Ed448). In particular: * It modifies prepare_tls_clienthello() to include Ed25519 and Ed448 in the signature_algorithms extension of the TLS 1.2 and earlier ClientHello (RFC 8422). * It modifies run_server_defaults() and get_server_certificate() to check whether the server offers EdDSA certificates with TLS 1.3. * It modifies certificate_info() to handle certificates signed with EdDSA or with EdDSA public keys, even if $OPENSSL does not support pretty printing such keys and signatures. * It modifies read_sigalg_from_file() to recognize EdDSA signatures even if $OPENSSL does not. --- CHANGELOG.md | 1 + testssl.sh | 70 +++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 54 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6641957..7ada38e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ * Don't use external pwd anymore * STARTTLS: XMPP server support * Rating (SSL Labs, not complete) +* Added support for certificates with EdDSA signatures and pubilc keys ### Features implemented / improvements in 3.0 diff --git a/testssl.sh b/testssl.sh index e29bda3..3f410d5 100755 --- a/testssl.sh +++ b/testssl.sh @@ -1040,7 +1040,7 @@ set_key_str_score() { # TODO: We need to get the size of DH params (follows the same table as the "else" clause) # For now, verifying the key size will do... - if [[ $type == EC ]]; then + if [[ $type == EC || $type == EdDSA ]]; then if [[ $size -lt 110 ]] && [[ $KEY_EXCH_SCORE -gt 20 ]]; then let KEY_EXCH_SCORE=20 set_grade_cap "F" "Using an insecure key" @@ -6249,7 +6249,15 @@ read_dhtype_from_file() { # arg1: certificate file read_sigalg_from_file() { - $OPENSSL x509 -noout -text -in "$1" 2>/dev/null | awk -F':' '/Signature Algorithm/ { print $2; exit; }' + local sig_alg + + sig_alg="$(strip_leading_space "$($OPENSSL x509 -noout -text -in "$1" 2>/dev/null | awk -F':' '/Signature Algorithm/ { print $2; exit; }')")" + case "$sig_alg" in + 1.3.101.112|ED25519) tm_out "Ed25519" ;; + 1.3.101.113|ED448) tm_out "Ed448" ;; + *) tm_out "$sig_alg" ;; + esac + } @@ -7547,7 +7555,7 @@ get_server_certificate() { CERTIFICATE_LIST_ORDERING_PROBLEM=false if [[ "$1" =~ "tls1_3" ]]; then [[ $(has_server_protocol "tls1_3") -eq 1 ]] && return 1 - if "$HAS_TLS13" && "$HAS_SIGALGS"; then + if "$HAS_TLS13" && "$HAS_SIGALGS" && [[ ! "$1" =~ "tls1_3_EdDSA" ]]; 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") $ERRFILE >$TMPFILE elif [[ "$1" =~ "tls1_3_ECDSA" ]]; then @@ -7568,6 +7576,8 @@ get_server_certificate() { 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" + elif [[ "$1" =~ "tls1_3_EdDSA" ]]; then + tls_sockets "04" "$TLS13_CIPHER" "all" "00,12,00,00, 00,05,00,05,01,00,00,00,00, 00,0d,00,06,00,04,08,07,08,08" else return 1 fi @@ -8332,8 +8342,16 @@ certificate_info() { GOOD_CA_BUNDLE="" cert_sig_algo="$(awk -F':' '/Signature Algorithm/ { print $2; if (++Match >= 1) exit; }' <<< "$cert_txt")" cert_sig_algo="${cert_sig_algo// /}" + case "$cert_sig_algo" in + 1.3.101.112|ED25519) cert_sig_algo="Ed25519" ;; + 1.3.101.113|ED448) cert_sig_algo="Ed448" ;; + esac cert_key_algo="$(awk -F':' '/Public Key Algorithm:/ { print $2; if (++Match >= 1) exit; }' <<< "$cert_txt")" cert_key_algo="${cert_key_algo// /}" + case "$cert_key_algo" in + 1.3.101.112|E[Dd]25519) cert_key_algo="Ed25519"; cert_keysize=253 ;; + 1.3.101.113|E[Dd]448) cert_key_algo="Ed448"; cert_keysize=456 ;; + esac out "$indent" ; pr_bold " Signature Algorithm " jsonID="cert_signatureAlgorithm" @@ -8441,6 +8459,10 @@ certificate_info() { fileout "${jsonID}${json_postfix}" "CRITICAL" "MD5" set_grade_cap "F" "Supports a insecure signature (MD5)" ;; + Ed25519|Ed448) + prln_svrty_good "$cert_sig_algo" + fileout "${jsonID}${json_postfix}" "OK" "$cert_sig_algo" + ;; *) out "$cert_sig_algo (" pr_warning "FIXME: can't tell whether this is good or not" @@ -8461,6 +8483,7 @@ certificate_info() { case $cert_key_algo in *RSA*|*rsa*) short_keyAlgo="RSA";; *ecdsa*|*ecPublicKey) short_keyAlgo="EC";; + *Ed25519*|*Ed448*) short_keyAlgo="EdDSA";; *DSA*|*dsa*) short_keyAlgo="DSA";; *GOST*|*gost*) short_keyAlgo="GOST";; *dh*|*DH*) short_keyAlgo="DH" ;; @@ -8523,6 +8546,10 @@ certificate_info() { ((ret++)) fi + set_key_str_score "$short_keyAlgo" "$cert_keysize" + elif [[ $cert_key_algo == Ed* ]]; then + pr_svrty_good "$cert_key_algo" + json_rating="OK"; json_msg="$short_keyAlgo $cert_key_algo" set_key_str_score "$short_keyAlgo" "$cert_keysize" else out "$cert_key_algo + $cert_keysize bits (" @@ -8586,7 +8613,7 @@ certificate_info() { 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 " ]] ) && \ + if ( [[ " $cert_type " =~ " RSASig " ]] || [[ " $cert_type " =~ " DSA " ]] || [[ " $cert_type " =~ " ECDSA " ]] || [[ " $cert_type " =~ " EdDSA " ]] ) && \ [[ ! "$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\"" @@ -9257,27 +9284,28 @@ run_server_defaults() { ciphers_to_test[7]="" ciphers_to_test[8]="tls1_3_RSA" ciphers_to_test[9]="tls1_3_ECDSA" + ciphers_to_test[10]="tls1_3_EdDSA" 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" + certificate_type[9]="ECDSA" ; certificate_type[10]="EdDSA" - for (( n=1; n <= 16 ; n++ )); do + for (( n=1; n <= 17 ; 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 + elif [[ $n -ge 11 ]]; 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]}" + [[ ${success[n-10]} -eq 0 ]] && [[ $(has_server_protocol "tls1_1") -ne 1 ]] && \ + ciphers_to_test[n]="${ciphers_to_test[n-10]}" && certificate_type[n]="${certificate_type[n-10]}" fi if [[ -n "${ciphers_to_test[n]}" ]]; then - if [[ $n -ge 10 ]]; then + if [[ $n -ge 11 ]]; then sni="$SNI" SNI="" get_server_certificate "${ciphers_to_test[n]}" "tls1_1" @@ -9288,7 +9316,7 @@ run_server_defaults() { success[n]=$? fi if [[ ${success[n]} -eq 0 ]] && [[ -s "$HOSTCERT" ]]; then - [[ $n -ge 10 ]] && [[ ! -e $HOSTCERT.nosni ]] && cp $HOSTCERT $HOSTCERT.nosni + [[ $n -ge 11 ]] && [[ ! -e $HOSTCERT.nosni ]] && cp $HOSTCERT $HOSTCERT.nosni cp "$TEMPDIR/$NODEIP.get_server_certificate.txt" $TMPFILE >$ERRFILE if [[ -z "$sessticket_lifetime_hint" ]]; then @@ -9370,7 +9398,7 @@ run_server_defaults() { fi i=$((i + 1)) done - if ! "$match_found" && [[ $n -ge 10 ]] && [[ $certs_found -ne 0 ]]; then + if ! "$match_found" && [[ $n -ge 11 ]] && [[ $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 @@ -9427,7 +9455,7 @@ run_server_defaults() { [[ -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" + [[ $n -ge 11 ]] && 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 @@ -10738,7 +10766,15 @@ get_pub_key_size() { "$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 2>>$ERRFILE | awk -F'(' '/Public-Key/ { print $2 }') + pubkeybits=$($OPENSSL x509 -noout -pubkey -in $HOSTCERT 2>>$ERRFILE | $OPENSSL pkey -pubin -text 2>>$ERRFILE) + if [[ "$pubkeybits" =~ E[Dd]25519 ]]; then + echo "Server public key is 253 bit" >> $TMPFILE + return 0 + elif [[ "$pubkeybits" =~ E[Dd]448 ]]; then + echo "Server public key is 456 bit" >> $TMPFILE + return 0 + fi + pubkeybits=$(awk -F'(' '/Public-Key/ { print $2 }' <<< "$pubkeybits") if [[ -n $pubkeybits ]]; then # remainder e.g. "256 bit)" pubkeybits="${pubkeybits//\)/}" @@ -14279,10 +14315,10 @@ prepare_tls_clienthello() { if [[ 0x$tls_low_byte -le 0x03 ]]; then extension_signature_algorithms=" - 00, 0d, # Type: signature_algorithms , see RFC 5246 - 00, 20, 00,1e, # lengths + 00, 0d, # Type: signature_algorithms , see RFC 5246 and RFC 8422 + 00, 24, 00,22, # 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" + 03,01, 03,02, 03,03, 02,01, 02,02, 02,03, 08,07, 08,08" else extension_signature_algorithms=" 00, 0d, # Type: signature_algorithms , see RFC 8446 From 37ffabf8d52a77cbb6da03ade93b53d063392d4a Mon Sep 17 00:00:00 2001 From: David Cooper Date: Mon, 18 May 2020 07:38:49 -0400 Subject: [PATCH 7/8] Remove some unnecessary quotation marks --- testssl.sh | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/testssl.sh b/testssl.sh index 3f410d5..c0ed0b4 100755 --- a/testssl.sh +++ b/testssl.sh @@ -7553,12 +7553,12 @@ get_server_certificate() { "$SSL_NATIVE" && using_sockets=false CERTIFICATE_LIST_ORDERING_PROBLEM=false - if [[ "$1" =~ "tls1_3" ]]; then + if [[ "$1" =~ tls1_3 ]]; then [[ $(has_server_protocol "tls1_3") -eq 1 ]] && return 1 - if "$HAS_TLS13" && "$HAS_SIGALGS" && [[ ! "$1" =~ "tls1_3_EdDSA" ]]; then - if [[ "$1" =~ "tls1_3_RSA" ]]; then + if "$HAS_TLS13" && "$HAS_SIGALGS" && [[ ! "$1" =~ tls1_3_EdDSA ]]; 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") $ERRFILE >$TMPFILE - elif [[ "$1" =~ "tls1_3_ECDSA" ]]; then + 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") $ERRFILE >$TMPFILE else return 1 @@ -7572,11 +7572,11 @@ get_server_certificate() { # For STARTTLS protcols 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 + 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 + 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" - elif [[ "$1" =~ "tls1_3_EdDSA" ]]; then + elif [[ "$1" =~ tls1_3_EdDSA ]]; then tls_sockets "04" "$TLS13_CIPHER" "all" "00,12,00,00, 00,05,00,05,01,00,00,00,00, 00,0d,00,06,00,04,08,07,08,08" else return 1 @@ -8613,19 +8613,19 @@ certificate_info() { 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_type " =~ " EdDSA " ]] ) && \ - [[ ! "$cert_keyusage" =~ "Digital Signature" ]]; then + if ( [[ " $cert_type " =~ \ RSASig\ ]] || [[ " $cert_type " =~ \ DSA\ ]] || [[ " $cert_type " =~ \ ECDSA\ ]] || [[ " $cert_type " =~ \ EdDSA\ ]] ) && \ + [[ ! "$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 + 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 + 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 From a4ae05c90c70063cd2eb9d4a3e25b60a4ec764b5 Mon Sep 17 00:00:00 2001 From: Dirk Date: Mon, 25 May 2020 13:23:49 +0200 Subject: [PATCH 8/8] Add get_txt_record(), fix variable declaration in get_mx_record() This commit adds a function for querying the TXT DNS record, so that subsequently we'll can build on top of that a function for checking MTA-STS, see #1073. Also it modifies a local variable mxs in get_mx_record() which was declared as mx but mxs was used. (That is pending an backport to 3.0.) --- testssl.sh | 40 ++++++++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/testssl.sh b/testssl.sh index c0ed0b4..2b34e85 100755 --- a/testssl.sh +++ b/testssl.sh @@ -19557,7 +19557,7 @@ get_caa_rr_record() { return 0 } -# watch out: $1 can also be a cname! --> all checked +# arg1: domain to check for. Returned will be the MX record as a string get_mx_record() { local mx="" local saved_openssl_conf="$OPENSSL_CONF" @@ -19567,21 +19567,49 @@ get_mx_record() { 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 }')" + mx="$(host -t MX "$1" 2>/dev/null | awk '/is handled by/ { print $(NF-1), $NF }')" elif "$HAS_DIG"; then - mxs="$(dig +short $noidnout -t MX "$1" 2>/dev/null | awk '/^[0-9]/ { print $1" "$2 }')" + mx="$(dig +short $noidnout -t MX "$1" 2>/dev/null | awk '/^[0-9]/ { print $1" "$2 }')" elif "$HAS_DRILL"; then - mxs="$(drill mx $1 | awk '/IN[ \t]MX[ \t]+/ { print $(NF-1), $NF }')" + mx="$(drill mx $1 | awk '/IN[ \t]MX[ \t]+/ { print $(NF-1), $NF }')" elif "$HAS_NSLOOKUP"; then - mxs="$(strip_lf "$(nslookup -type=MX "$1" 2>/dev/null | awk '/mail exchanger/ { print $(NF-1), $NF }')")" + mx="$(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" + echo "$mx" } +# arg1: domain / hostname. Returned will be the TXT record as a string which can be multilined +# (one entry per line), for e.g. non-MTA-STS records. +# Is supposed to be used by MTA STS in the future like get_txt_record _mta-sts.DOMAIN.TLD +get_txt_record() { + local record="" + 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 and strip any remaining double quotes later + if "$HAS_HOST"; then + record="$(host -t TXT "$1" 2>/dev/null | awk -F\" '/descriptive text/ { print $(NF-1) }')" + elif "$HAS_DIG"; then + record="$(dig +short $noidnout -t TXT "$1" 2>/dev/null)" + elif "$HAS_DRILL"; then + record="$(drill txt $1 | awk -F\" '/^[a-z0-9].*TXT/ { print $(NF-1) }')" + elif "$HAS_NSLOOKUP"; then + record="$(strip_lf "$(nslookup -type=MX "$1" 2>/dev/null | awk -F= '/text/ { 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 "${record//\"/}" +} + + # set IPADDRs and IP46ADDRs #