diff --git a/testssl.sh b/testssl.sh index df23a07..60d8a4d 100755 --- a/testssl.sh +++ b/testssl.sh @@ -2359,7 +2359,7 @@ hex2binary() { # convert 414243 into ABC hex2ascii() { - hex2binary $1 + hex2binary "$1" } # arg1: text string @@ -22622,7 +22622,8 @@ get_caa_rrecord() { # get_https_rrecord() { local raw_https="" - local hash="" len line="" + local hash="" line="" + local len # better fix as integer? local len_alpnID="" local alpnID="" local alpnID_wire="" @@ -22634,42 +22635,36 @@ get_https_rrecord() { [[ -n "$NODNS" ]] && return 2 # if minimum DNS lookup was instructed, leave here "$HAS_DIG_NOIDNOUT" && noidnout="+noidnout" - # There's a) the possibility to query HTTPS RR records directly like "dig +short HTTPS dev.testssl.sh", - # "drill HTTPS FQDN" or "nslookup -type=HTTPS FQDN". This works for newer binaries only, unfortunately. - # On top of that b) there's also an extended format which e.g. cloudflare uses: - # $ host -t type65 testssl.net - # testssl.net has TYPE65 record \# 136 00010000010006026833026832000400086815229AAC43CDE7000500 470045FE0D0041A70020002057F87361C7B5A3B8CD3C028892690D35 2863623DAD4E03D33B231A4C3C8BB02B0004000100010012636C6F75 64666C6172652D6563682E636F6D0000000600202606470030310000 00000000AC43CDE72606470030360000000000006815229A - # $ host -t HTTPS testssl.net - # testssl.net has HTTPS record 1 . alpn="h3,h2" ipv4hint=104.21.34.154,172.67.205.231 ech=AEX+DQBBpwAgACBX+HNhx7WjuM08AoiSaQ01KGNiPa1OA9M7IxpMPIuwKwAEAAEAAQASY2xvdWRmbGFyZS1lY2guY29tAAA= ipv6hint=2606:4700:3031::ac43:cde7,2606:4700:3036::6815:229a - # ECH is the encrypted client hello --> for esni (https://datatracker.ietf.org/doc/draft-ietf-tls-esni/) - # Nice description: https://www.netmeister.org/blog/https-rrs.html - - # Thus we try first whether we can query the HTTPS records directly as this gives us that already - # in clear text and also we can avoid to parse the encoded format. We'll do that as a fallback but - # at this moment we're trying to scrape only the values alpn from it, if they come first. + # There's the possibility to query HTTPS RR records directly like "dig +short HTTPS dev.testssl.sh", + # "drill HTTPS FQDN" or "nslookup -type=HTTPS FQDN". This works for new binaries only. Thus we try first + # whether we can query the HTTPS records directly as this gives us that already everything we want in + # in clear text and also we can avoid to parse the encoded formats. + # "tail -1" and the awk commands make sure we use the right lines when we encounter a CNAME OPENSSL_CONF="" if "$HAS_DIG_HTTPS"; then - text_httpsrr=$(dig +short +search +timeout=3 +tries=3 $noidnout HTTPS "$1" 2>/dev/null) + text_httpsrr="$(dig +short +search +timeout=3 +tries=3 $noidnout HTTPS "$1" 2>/dev/null | tail -1)" elif "$HAS_DRILL_HTTPS"; then - text_httpsrr=$(drill -Q HTTPS $1 2>/dev/null) + text_httpsrr="$(drill -Q HTTPS "$1" 2>/dev/null | tail -1)" elif "$HAS_HOST_HTTPS"; then - text_httpsrr=$(host -t HTTPS $1 2>/dev/null) - text_httpsrr=${text_httpsrr#*record } - elif "$HAS_NSLOOKUP_HTTPS"; then # from 4th field onwards \/ - text_httpsrr=$(nslookup -type=HTTPS $1 | awk '/'"^${1}"'.*rdata_65// { print substr($0,index($0,$4)) }') + text_httpsrr="$(host -t HTTPS "$1" 2>/dev/null | awk -F'HTTP service bindings ' '/HTTP service bindings /{print $2}')" + elif "$HAS_NSLOOKUP_HTTPS"; then + text_httpsrr="$(nslookup -type=HTTPS "$1" | awk -F'rdata_65 = ' '/rdata_65 =/{print $2}' )" fi if [[ -n "$text_httpsrr" ]]; then safe_echo "$text_httpsrr" + OPENSSL_CONF="$saved_openssl_conf" # see https://github.com/drwetter/testssl.sh/issues/134 + return 0 + elif "$HAS_DIG_HTTPS" || "$HAS_DRILL_HTTPS" || "$HAS_HOST_HTTPS" || "$HAS_NSLOOKUP_HTTPS"; then + # no record despite binaries are "HTTPS record aware" + OPENSSL_CONF="$saved_openssl_conf" return 0 fi - # Now we need to try parsing the raw output - # Format probably: https://www.rfc-editor.org/rfc/rfc3597 (plus updates) - - # If there's a type65 record there are 2x3 output formats, mostly depending on age of distribution - # -- roughly that's the difference between text and binary format -- and the type of DNS client + # As we didn't succeed yet, we need to try parsing the raw output. First is to get the TYPE65 record + # as text. These days (2026) it's not that common anymore. Mac is the party pooper as it normally returns + # a hex stream only --in 2026. Here's how output of old+ancient client DNS binaries may look like with TYPE65 # for host: # 1) 'google.com has HTTPS record 1 . alpn="h2,h3" ' @@ -22687,10 +22682,10 @@ get_https_rrecord() { raw_https="$(dig $DIG_R +short +search +timeout=3 +tries=3 $noidnout type65 "$1" 2>/dev/null)" # empty if there's no such record elif "$HAS_DRILL"; then - raw_https="$(drill $1 type65 | grep -v '^;;' | awk '/'"^${1}"'.*TYPE65/ { print substr($0,index($0,$5)) }' )" # from 5th field onwards + raw_https="$(drill "$1" type65 | grep -v '^;;' | awk '/'"^${1}"'.*TYPE65/ { print substr($0,index($0,$5)) }' )" # from 5th field onwards # empty if there's no such record elif "$HAS_HOST"; then - raw_https="$(host -t type65 $1)" + raw_https="$(host -t type65 "$1")" if [[ "$raw_https" =~ "has no HTTPS|has no TYPE65" ]]; then raw_https="" else @@ -22698,76 +22693,82 @@ get_https_rrecord() { raw_https="${raw_https/$1 has TYPE65 record /}" fi elif "$HAS_NSLOOKUP"; then - raw_https="$(strip_lf "$(nslookup -type=type65 $1 | awk '/'"^${1}"'.*rdata_65/ { print substr($0,index($0,$4)) }' )")" + raw_https="$(strip_lf "$(nslookup -type=type65 "$1" | awk '/'"^${1}"'.*rdata_65/ { print substr($0,index($0,$4)) }' )")" # empty if there's no such record 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 + OPENSSL_CONF="$saved_openssl_conf" # We're done now with openssl, see https://github.com/drwetter/testssl.sh/issues/134 -# dig +short HTTPS dev.testssl.sh / dig +short type65 dev.testssl.sh -# 1 . alpn="h2" port=443 ipv6hint=2a01:238:4308:a920:1000:0:b:1337 -# -# 36 000100000100030268320003000201BB000600102A0102384308A920 10000000000B1337 -# alpn| L h 2 443 2a010238... L=len -# -# ----------------- -# testssl.net (split over a couple of lines) -# -# 1. alpn="h3,h2" ipv4hint=104.21.34.154,172.67.205.231 -# 136 00010000010006026833026832000400086815229AAC43CDE7000500 470045FE0D0041F3002000202BD0935ED66980C1862F2570C0D6014D -# alpn| L h 3 L h 2 |IPv4#1||IPv4#2| + # Now comes the third, more tricky part and the last straw --> parsing the hex stream which was returned if it was returned. + # Format is like: https://www.rfc-editor.org/rfc/rfc3597 (plus updates) -# ech=AEX+DQBBzgAgACBQGA9EFbz+PkJAXSXtcqJluxLlhxIgzhJ+GhTtRd4nJQAEAAEAAQASY2xvdWRmbGFyZS1lY2guY29tAAA= ipv6hint=2606:4700:3031::ac43:cde7,2606:4700:3036::6815:229a -# 733A7CFAAEA5E4DD9CA43D4C24199E330004000100010012636C6F75 64666C6172652D6563682E636F6D0000000600202606470030310000 00000000AC43CDE72606470030360000000000006815229A -# | cloudflare-ech.com | IPv6#1 #IPv6#2 + # dev.testssl.sh: + # 1 . alpn="h2" port=443 ipv6hint=2a01:238:4308:a920:1000:0:b:1337 + # + # 36 000100000100030268320003000201BB000600102A0102384308A920 10000000000B1337 + # TL alpn| L h 2 443 2a010238... L=len of alpn entries, TL=total length of the following by, excluding spaces + # + # ----------------- + # testssl.net (here hown over a couple of lines): + # 1 . alpn="h3,h2" ipv4hint=104.21.34.154,172.67.205.231 ech=AEX+DQBBpwAgACBX+HNhx7WjuM08AoiSaQ01KGNiPa1OA9M7IxpMPIuwKwAEAAEAAQASY2xvdWRmbGFyZS1lY2guY29tAAA= ipv6hint=2606:4700:3031::ac43:cde7,2606:4700:3036::6815:229a + # ECH is the encrypted client hello --> for esni (https://datatracker.ietf.org/doc/draft-ietf-tls-esni/) + # Nice description: https://www.netmeister.org/blog/https-rrs.html + # + # 1. alpn="h3,h2" ipv4hint=104.21.34.154,172.67.205.231 + # 136 00010000010006026833026832000400086815229AAC43CDE7000500 470045FE0D0041F3002000202BD0935ED66980C1862F2570C0D6014D + # TL alpn| L h 3 L h 2 |IPv4#1||IPv4#2| + # + # ech=AEX+DQBBzgAgACBQGA9EFbz+PkJAXSXtcqJluxLlhxIgzhJ+GhTtRd4nJQAEAAEAAQASY2xvdWRmbGFyZS1lY2guY29tAAA= ipv6hint=2606:4700:3031::ac43:cde7,2606:4700:3036::6815:229a + # 733A7CFAAEA5E4DD9CA43D4C24199E330004000100010012636C6F75 64666C6172652D6563682E636F6D0000000600202606470030310000 00000000AC43CDE72606470030360000000000006815229A + # | cloudflare-ech.com | IPv6#1 #IPv6#2 - -# Now comes the last straw, decoding og the stream. It only works for short entries like 1. alpn=h2,h3 + # Be aware that all variables are strings here! Therefore we use double quotes so that e.g. 03 won't become 3 + # For now the following only works for short entries like 1. alpn=h2,h3 if [[ -z "$raw_https" ]]; then return 1 elif [[ "$raw_https" =~ \#\ [0-9][0-9] ]]; then - while read hash len line ;do + # signals that we're on the right track with type65 interpretation + read hash len line <<< "$raw_https" # \# 10 00010000010003026832 # \# 36 000100000100030268320003000201BB000600102A01023842816755 10000000000B1337 [[ $DEBUG -eq 1 ]] && echo "$hash $len $line" + if [[ "${line:0:4}" == 0001 ]]; then # marker to proceed, belongs to SvcPriority, see rfc9460, 2.4.3 svc_priority=$(printf "%0d" "$((10#${line:2:2}))") # 1 is most often, 0 is alias - if [[ $svc_priority == 1 ]]; then + if [[ $svc_priority == "1" ]]; then # mock text representation - svc_priority="$svc_priority . " + svc_priority+=" . " alpnID="${alpnID}${svc_priority}" fi - if [[ ${line:8:2} == 01 ]]; then # Then comes SvcParamKeys, see rfc 14.3.2 which should be alpn=1 - alpnID="${alpnID}alpn=\"" # double quote for clear text + if [[ ${line:8:2} == "01" ]]; then # Then comes SvcParamKeys, see rfc 14.3.2 which should be alpn=1 + alpnID+="\"" # double quote for clear text else - continue # If the 1st element is not alpn, next iteration of loop will fail. + continue # If the 1st element is not alpn, next iteration of loop will fail for now fi # Should we care as SvcParamKey!=alpn doesn't seems not very common? - len_alpnID=${line:12:2} # length of alpn entries (1st?) - alpnID_wire=${line:16:4} # value of first entry - alpnID=${alpnID}$(hex2ascii $alpnID_wire) -# from here it only works for one simple entry (like h2 or h2,h3) - if [[ "$len_alpnID" != "03" ]]; then # 06 would be another entry e.g. h3, quote the rhs! - alpnID_wire=${line:22:4} #FIXME: we can't cope with three entries yet - alpnID="${alpnID},$(hex2ascii $alpnID_wire)" + len_alpnID="${line:12:2}" # length of alpn entries, e.g. 03 or 06 + alpnID_wire="${line:16:4}" # value of first entry + alpnID="${alpnID}$(hex2ascii "$alpnID_wire")" + localPTR=20 + if [[ "$len_alpnID" == "06" ]]; then # 06 would be another entry e.g. h3, quote the rhs! + alpnID_wire="${line:22:4}" #FIXME: we can't cope with three entries yet + alpnID="${alpnID},$(hex2ascii "$alpnID_wire")" + localPTR=26 fi - [[ ${line:8:2} == 01 ]] && alpnID="${alpnID}\"" # if alpn add trailing double quote + [[ ${line:8:2} == "01" ]] && alpnID+="\"" # if alpn and we're done add trailing double quote -# best is to check len and then stop now - if [[ $len -eq 10 ]]; then - [[ $DEBUG -eq 1 ]] && echo "end 10" - break - fi + # done if localPTR / 2 = len or localPTR not empty + echo "key: ${line:localPTR:4}" + # key=1: alpn + # key=3: port + # key=4: IPv4 + # key=6: IPv6 -# len_alpnID=$((len_alpnID*2)) # =>word! Now get name from 4th and value from 4th+len position... -# alpnID="$(hex2ascii ${line:4:$len_alpnID})" -# alpnID_wire="$(hex2ascii "${line:$((4+len_alpnID)):100}")" else out "please report unknown HTTPS RR $line with flag @ $NODE" return 7 fi - done <<< "$raw_https" safe_echo "$alpnID" fi return 0 @@ -23602,6 +23603,7 @@ dns_https_rr () { local jsonID="DNS_HTTPS_rrecord" local https_rr="" local indent="" + local https_rr_node="$NODE" out "$indent"; pr_bold " DNS HTTPS RR"; out " (expt.): " if [[ -n "$NODNS" ]]; then @@ -23611,9 +23613,11 @@ dns_https_rr () { out "(instructed to use the proxy for DNS only)" fileout "${jsonID}" "INFO" "check skipped as instructed (proxy)" else - https_rr="$(get_https_rrecord $NODE)" + # append a dot if there was none + [[ $https_rr_node =~ '.'$ ]] || https_rr_node+="." + https_rr="$(get_https_rrecord $https_rr_node)" if [[ -n "$https_rr" ]]; then - pr_svrty_good "yes" ; out ": " + pr_svrty_good "yes" ; out ": " prln_italic "$(out_row_aligned_max_width "$https_rr" "$indent " $TERM_WIDTH)" fileout "${jsonID}" "OK" "$https_rr" else @@ -23624,7 +23628,6 @@ dns_https_rr () { } - # Check messages which needed to be processed. I.e. those which would have destroyed the nice # screen output and thus havve been postponed. This is just an idea and is only used once # but can be extended in the future. An array might be more handy