From e0c0a6658f5dbcdc7dd753135b8d8e514fcd6d1f Mon Sep 17 00:00:00 2001
From: Dirk Wetter
Date: Sat, 30 May 2026 17:40:34 +0200
Subject: [PATCH 01/13] Provide HTTPS RR functionality
This is a fresh start for #2484 as the PR wasn't ready yet for 3.2 by the time it was released. And it continues #2866
which was kind of messed up by accident.
The info for the HTTPS RR shows up in the very beginning, i.e. in `service_detection()`. All keys are listed now in bold, values in a regular font.
`get_https_rrecord()` was introduced by copying and modifying `get_caa_rr_record()`.
There's a similar obstacle as with CAA RRs: older binaries show the resource records binary encoded. Thus a new set of global vars is introduced HAS_*_HTTPS which check whether the binaries support decoding the RR directly. As of now raw decoding doesn't work completely.
Todo:
- Add logic in QUIC
- if RR is detected and not QUIC is possible
- add time for QUIC detection when RR is retrieved
- show full HTTPS RR record, at least when having a new DNS client
- coninue with raw decoding, if possible (otherwise problematic for MacOS)
- shorten the comments in `get_https_rrecord()`
- man page
- when ASSUME_HTTP is set and no services was detected: this needs to be handled
- The placement of the output should be reconsidered and/or cached when multiple IPs belong to a FQDN
---
.github/workflows/codespell.yml | 4 +-
CHANGELOG.md | 1 +
t/baseline_data/default_testssl.csvfile | 1 +
testssl.sh | 272 ++++++++++++++++++++++--
4 files changed, 256 insertions(+), 22 deletions(-)
diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml
index 18ff48d..c0177b5 100644
--- a/.github/workflows/codespell.yml
+++ b/.github/workflows/codespell.yml
@@ -12,5 +12,5 @@ jobs:
- uses: actions/checkout@v6
- uses: codespell-project/actions-codespell@master
with:
- skip: ca_hashes.txt,tls_data.txt,*.pem,OPENSSL-LICENSE.txt,CREDITS.md,openssl.cnf,fedora-dirk-ipv6.diff,testssl.1
- ignore_words_list: borken,gost,ciph,ba,bloc,isnt,chello,fo,alle,anull
+ skip: ca_hashes.txt,tls_data.txt,*.pem,OPENSSL-LICENSE.txt,CREDITS.md,openssl.cnf,testssl.1
+ ignore_words_list: borken,gost,ciph,ba,bloc,isnt,chello,fo,alle,anull,expt
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1e725f7..d98a7b5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@
* Bump SSLlabs rating guide to 2009r
* Check for Opossum vulnerability
* Enable IPv6 automagically, i.e. if target via IPv6 is reachable just (also) scan it
+* Detect and show DNS HTTPS RR (RFC 9460)
* Provide an FAQ
### Features implemented / improvements in 3.2
diff --git a/t/baseline_data/default_testssl.csvfile b/t/baseline_data/default_testssl.csvfile
index f43bbb9..a37854e 100644
--- a/t/baseline_data/default_testssl.csvfile
+++ b/t/baseline_data/default_testssl.csvfile
@@ -1,5 +1,6 @@
"id","fqdn/ip","port","severity","finding","cve","cwe"
"engine_problem","/","443","WARN","No engine or GOST support via engine with your ./bin/openssl.Linux.x86_64","",""
+"DNS_HTTPS_rrecord","testssl.sh/81.169.166.184","443","OK","1 . alpn='h2'","",""
"service","testssl.sh/81.169.235.32","443","INFO","HTTP","",""
"pre_128cipher","testssl.sh/81.169.235.32","443","INFO","No 128 cipher limit bug","",""
"SSLv2","testssl.sh/81.169.235.32","443","OK","not offered","",""
diff --git a/testssl.sh b/testssl.sh
index 18c741a..df23a07 100755
--- a/testssl.sh
+++ b/testssl.sh
@@ -387,6 +387,11 @@ HAS_IDN2=false
HAS_AVAHIRESOLVE=false
HAS_DSCACHEUTIL=false
HAS_DIG_NOIDNOUT=false
+HAS_DIG_HTTPS=false # *_HTTPS: whether the binaries support HTTPS RR directly
+HAS_DRILL_HTTPS=false
+HAS_HOST_HTTPS=false
+HAS_NSLOOKUP_HTTPS=false
+
HAS_XXD=false
OSSL_CIPHERS_S=""
@@ -2501,6 +2506,7 @@ s_client_options() {
# 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.
+# sets global $SERVICE
#
service_detection() {
local -i was_killed
@@ -2545,24 +2551,29 @@ service_detection() {
debugme head -50 $TMPFILE | sed -e '//,$d' -e '//,$d' -e '/ trying HTTP checks"
SERVICE=HTTP
fileout "${jsonID}" "DEBUG" "Couldn't determine service -- ASSUME_HTTP set"
elif [[ "$CLIENT_AUTH" == required ]] && [[ -z $MTLS ]]; then
- out " certificate-based authentication without providing client certificate and private key => skipping all HTTP checks"
- echo "certificate-based authentication without providing client certificate and private key => skipping all HTTP checks" >$TMPFILE
+ out " certificate-based authentication without providing client certificate and private key => skipping all HTTP checks" | tee $TMPFILE
fileout "${jsonID}" "INFO" "certificate-based authentication without providing client certificate and private key => skipping all HTTP checks"
else
out " Couldn't determine what's running on port $PORT"
@@ -9396,6 +9407,7 @@ certificate_info() {
local first=true
local badocsp=1
local len_cert_serial=0
+ local avoid_complaints="^(1\.1\.1\.1|1\.0\.0\.1|8\.8\.8\.8|8\.8\.4\.4|9\.9\.9\.9)$"
if [[ $number_of_certificates -gt 1 ]]; then
[[ $certificate_number -eq 1 ]] && outln
@@ -10273,9 +10285,9 @@ certificate_info() {
out "$indent"; pr_bold " DNS CAA RR"; out " (experimental) "
jsonID="DNS_CAArecord"
- if is_ipv4addr "$NODE" || is_ipv6addr "$NODE"; then
- out "not checked (IP address scan -- no domain to query)"
- fileout "${jsonID}${json_postfix}" "INFO" "not checked (IP address scan)"
+ if [[ ! $tmp =~ [a-zA-Z] ]] && [[ ! $tmp =~ $avoid_complaints ]]; then
+ out "not checked: IP address scan, no domain to query"
+ fileout "${jsonID}${json_postfix}" "INFO" "not checked IP address scan, no domain to query"
else
caa_node="$NODE"
caa=""
@@ -17732,7 +17744,7 @@ run_ticketbleed() {
local hint=""
[[ -n "$STARTTLS" ]] && return 0
- pr_bold " Ticketbleed"; out " ($cve), experiment. "
+ pr_bold " Ticketbleed"; out " ($cve), experimental "
if [[ "$SERVICE" != HTTP ]] && [[ "$CLIENT_AUTH" != required ]]; then
outln "(applicable only for HTTP service)"
@@ -22353,6 +22365,8 @@ get_local_a() {
#
check_resolver_bins() {
local saved_openssl_conf="$OPENSSL_CONF"
+ local testhost=localhost
+ local str=""
OPENSSL_CONF="" # see https://github.com/testssl/testssl.sh/issues/134
type -p dig &> /dev/null && HAS_DIG=true
@@ -22377,12 +22391,36 @@ check_resolver_bins() {
HAS_DIG_NOIDNOUT=true
fi
fi
+
+ # Pre-checking the following for HTTPS RR, see get_https_rrecord()
+ if "$HAS_DIG"; then
+ str=$(dig +short $testhost HTTPS)
+ if [[ -z "$str" ]] && [[ ! "$str" =~ 127.0.0.1 ]] ; then
+ HAS_DIG_HTTPS=true
+ fi
+ elif "$HAS_DRILL"; then
+ if drill $testhost HTTPS | grep -Eq 'IN.*HTTPS'; then
+ HAS_DRILL_HTTPS=true
+ fi
+ elif "$HAS_HOST"; then
+ host -t HTTPS $testhost 2>&1 | grep -q 'invalid type'
+ if [[ $? -ne 0 ]]; then
+ HAS_HOST_HTTPS=true
+ fi
+ elif "$HAS_NSLOOKUP"; then
+ nslookup -type=HTTPS $testhost | grep -q 'unknown query type'
+ if [[ $? -ne 0 ]]; then
+ HAS_NSLOOKUP_HTTPS=true
+ fi
+ fi
+
OPENSSL_CONF="$saved_openssl_conf" # see https://github.com/testssl/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"
@@ -22436,6 +22474,7 @@ get_a_record() {
# 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"
@@ -22485,9 +22524,12 @@ get_aaaa_record() {
echo "$ip6"
}
+
# RFC6844: DNS Certification Authority Authorization (CAA) Resource Record
# arg1: domain to check for
-get_caa_rr_record() {
+#FIXME: should be refactored, see get_https_rrecord()
+#
+get_caa_rrecord() {
local raw_caa=""
local hash len line
local -i len_caa_property
@@ -22515,12 +22557,16 @@ get_caa_rr_record() {
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")"
+ if [[ "$raw_caa" =~ "has no CAA|has no TYPE257" ]]; then
+ raw_caa=""
+ else
+ raw_caa="${raw_caa/$1 has CAA record /}"
+ raw_caa="${raw_caa/$1 has TYPE257 record /}"
fi
elif "$HAS_NSLOOKUP"; then
raw_caa="$(strip_lf "$(nslookup -type=type257 $1 | grep -w rdata_257)")"
if [[ -n "$raw_caa" ]]; then
+ #FIXME: modernize here or see HTTPS RR
raw_caa="$(sed 's/^.*rdata_257 = //' <<< "$raw_caa")"
fi
else
@@ -22563,11 +22609,171 @@ get_caa_rr_record() {
return 1
fi
-# to do:
+#TODO:
# 4: check whether $1 is a CNAME and take this
return 0
}
+
+# Service Binding and Parameter Specification via the DNS (SVCB and HTTPS Resource Records).
+# https://www.rfc-editor.org/rfc/rfc9460.html
+# arg1: domain to check for
+# returns: string for record
+#
+get_https_rrecord() {
+ local raw_https=""
+ local hash="" len line=""
+ local len_alpnID=""
+ local alpnID=""
+ local alpnID_wire=""
+ local saved_openssl_conf="$OPENSSL_CONF"
+ local all_https=""
+ local noidnout=""
+ local svc_priority=""
+
+ [[ -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.
+
+ OPENSSL_CONF=""
+ if "$HAS_DIG_HTTPS"; then
+ text_httpsrr=$(dig +short +search +timeout=3 +tries=3 $noidnout HTTPS "$1" 2>/dev/null)
+ elif "$HAS_DRILL_HTTPS"; then
+ text_httpsrr=$(drill -Q HTTPS $1 2>/dev/null)
+ 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)) }')
+ fi
+
+ if [[ -n "$text_httpsrr" ]]; then
+ safe_echo "$text_httpsrr"
+ 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
+
+ # for host:
+ # 1) 'google.com has HTTPS record 1 . alpn="h2,h3" '
+ # 2) 'google.com has TYPE65 record \# 13 0001000001000602683202683 '
+
+ # for drill and dig it's like
+ #1) google.com. 18665 IN TYPE65 \# 13 00010000010006026832026833
+ #2) google.com. 18301 IN HTTPS 1 . alpn="h2,h3"
+
+ # nslookup:
+ # 1) dev.testssl.sh rdata_65 = 1 . alpn="h2"
+ # 2) dev.testssl.sh rdata_65 = \# 10 00010000010003026832
+
+ if "$HAS_DIG"; then
+ 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
+ # empty if there's no such record
+ elif "$HAS_HOST"; then
+ raw_https="$(host -t type65 $1)"
+ if [[ "$raw_https" =~ "has no HTTPS|has no TYPE65" ]]; then
+ raw_https=""
+ else
+ raw_https="${raw_https/$1 has HTTPS record /}"
+ 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)) }' )")"
+ # 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
+
+# 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|
+
+# 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
+ if [[ -z "$raw_https" ]]; then
+ return 1
+ elif [[ "$raw_https" =~ \#\ [0-9][0-9] ]]; then
+ while read hash len line ;do
+ # \# 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
+ # mock text representation
+ 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
+ else
+ continue # If the 1st element is not alpn, next iteration of loop will fail.
+ 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)"
+ fi
+ [[ ${line:8:2} == 01 ]] && alpnID="${alpnID}\"" # if alpn 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
+
+# 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
+}
+
+
# arg1: domain to check for. Returned will be the MX record as a string
get_mx_record() {
local mx=""
@@ -23392,6 +23598,33 @@ determine_optimal_proto() {
}
+dns_https_rr () {
+ local jsonID="DNS_HTTPS_rrecord"
+ local https_rr=""
+ local indent=""
+
+ out "$indent"; pr_bold " DNS HTTPS RR"; out " (expt.): "
+ if [[ -n "$NODNS" ]]; then
+ out "(instructed to minimize/skip DNS queries)"
+ fileout "${jsonID}" "INFO" "check skipped as instructed"
+ elif "$DNS_VIA_PROXY"; then
+ out "(instructed to use the proxy for DNS only)"
+ fileout "${jsonID}" "INFO" "check skipped as instructed (proxy)"
+ else
+ https_rr="$(get_https_rrecord $NODE)"
+ if [[ -n "$https_rr" ]]; then
+ pr_svrty_good "yes" ; out ": "
+ prln_italic "$(out_row_aligned_max_width "$https_rr" "$indent " $TERM_WIDTH)"
+ fileout "${jsonID}" "OK" "$https_rr"
+ else
+ outln "--"
+ fileout "${jsonID}" "INFO" " no resource record found"
+ fi
+ fi
+}
+
+
+
# 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
@@ -23442,7 +23675,7 @@ determine_service() {
fi
GET_REQ11="GET $URL_PATH HTTP/1.1\r\nHost: $NODE\r\nUser-Agent: $ua\r\n${basicauth_header}${reqheader}Accept-Encoding: identity\r\nAccept: */*\r\nConnection: Close\r\n\r\n"
determine_optimal_proto
- # returns always 0:
+ # returns always 0 and sets $SERVICE
service_detection $OPTIMAL_PROTO
check_msg
else # STARTTLS
@@ -23515,7 +23748,7 @@ determine_service() {
determine_optimal_sockets_params
determine_optimal_proto "$1"
- out " Service set:$CORRECT_SPACES STARTTLS via "
+ pr_bold " Service set"; out ":$CORRECT_SPACES STARTTLS via "
out "$(toupper "$protocol")"
[[ "$protocol" == mysql ]] && out " (experimental)"
fileout "service" "INFO" "$protocol"
@@ -23529,7 +23762,6 @@ determine_service() {
# It comes handy later also for STARTTLS injection to define this global. When we do banner grabbing
# or replace service_detection() we might not need that anymore
SERVICE=$protocol
-
fi
tmpfile_handle ${FUNCNAME[0]}.txt
@@ -23595,7 +23827,7 @@ display_rdns_etc() {
outln "$PROXYIP:$PROXYPORT "
fi
if [[ $(count_words "$IPADDRs2SHOW") -gt 1 ]]; then
- out " Further IP addresses: $CORRECT_SPACES"
+ pr_bold " Further IP addresses"; out ": $CORRECT_SPACES"
for ip in $IPADDRs2SHOW; do
if [[ "$ip" == $NODEIP ]] || [[ "[$ip]" == $NODEIP ]]; then
continue
@@ -23616,11 +23848,11 @@ display_rdns_etc() {
outln " A record via: $CORRECT_SPACES supplied IP \"$CMDLINE_IP\""
fi
fi
+ pr_bold " rDNS "
+ out "$(printf "%-19s" "($nodeip):")"
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
}
From e365ccf03ff879bbd5396788eea7440a3515134e Mon Sep 17 00:00:00 2001
From: Dirk Wetter
Date: Sun, 31 May 2026 19:59:01 +0200
Subject: [PATCH 02/13] try to squash the baseline comparison check
---
t/baseline_data/default_testssl.csvfile | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/t/baseline_data/default_testssl.csvfile b/t/baseline_data/default_testssl.csvfile
index a37854e..155ccf7 100644
--- a/t/baseline_data/default_testssl.csvfile
+++ b/t/baseline_data/default_testssl.csvfile
@@ -1,6 +1,6 @@
"id","fqdn/ip","port","severity","finding","cve","cwe"
"engine_problem","/","443","WARN","No engine or GOST support via engine with your ./bin/openssl.Linux.x86_64","",""
-"DNS_HTTPS_rrecord","testssl.sh/81.169.166.184","443","OK","1 . alpn='h2'","",""
+"DNS_HTTPS_rrecord","testssl.sh/81.169.235.32","443","OK","1 . alpn='h2'","",""
"service","testssl.sh/81.169.235.32","443","INFO","HTTP","",""
"pre_128cipher","testssl.sh/81.169.235.32","443","INFO","No 128 cipher limit bug","",""
"SSLv2","testssl.sh/81.169.235.32","443","OK","not offered","",""
From ba7d9604a90d78d348adec8278e86b876dfb9cbc Mon Sep 17 00:00:00 2001
From: Dirk Wetter
Date: Mon, 1 Jun 2026 10:14:25 +0200
Subject: [PATCH 03/13] Getting from github runner under MacOS
as there is an inexplicable difference between a real Mac
which passes the run and the one in github
-"DNS_HTTPS_rrecord","testssl.sh/81.169.235.32","443","OK","81.169.235.32","",""
+"DNS_HTTPS_rrecord","testssl.sh/81.169.235.32","443","OK","1 . alpn='h2'","",""
The first line comes from the runner
---
.github/workflows/unit_tests_macos.yml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/.github/workflows/unit_tests_macos.yml b/.github/workflows/unit_tests_macos.yml
index 0cf273d..8d68356 100644
--- a/.github/workflows/unit_tests_macos.yml
+++ b/.github/workflows/unit_tests_macos.yml
@@ -45,6 +45,8 @@ jobs:
printf "%s\n" "----------"
bash --version
printf "%s\n" "----------"
+ echo $PATH
+ printf "%s\n" "----------"
- name: Install perl modules
run: |
From 84bd9dd1a3681c2200722ee2c14da2c2f2093bcb Mon Sep 17 00:00:00 2001
From: Dirk Wetter
Date: Mon, 1 Jun 2026 16:14:29 +0200
Subject: [PATCH 04/13] Updatesr get_https_rrecord()
- quote vars (hoping it'll resolve the Mac runner issue)
- make sure CNAMEs are properly parsed
- end get_https_rrecord() earlier when there's no record but DNS binaries are "HTTPS record aware"
- while loop was redundant
- better comments
Elsewhere:
make sure get_https_rrecord is called with a trailing dot for the NODE
---
testssl.sh | 149 +++++++++++++++++++++++++++--------------------------
1 file changed, 76 insertions(+), 73 deletions(-)
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
From a92cd8f702972379f00c5f2d2489b2cde7a937ee Mon Sep 17 00:00:00 2001
From: Dirk Wetter
Date: Mon, 1 Jun 2026 16:56:47 +0200
Subject: [PATCH 05/13] fix shellcheck complaint
---
testssl.sh | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/testssl.sh b/testssl.sh
index 60d8a4d..2476b32 100755
--- a/testssl.sh
+++ b/testssl.sh
@@ -22744,9 +22744,7 @@ get_https_rrecord() {
fi
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 for now
- fi # Should we care as SvcParamKey!=alpn doesn't seems not very common?
+ fi
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")"
From 51ba8327a89d1b4f255203a3ade65e3c8b39ebb7 Mon Sep 17 00:00:00 2001
From: Dirk Wetter
Date: Tue, 2 Jun 2026 19:04:17 +0200
Subject: [PATCH 06/13] introduce subfunctions decode_*
First implemented and tested working is decode_https_rr_alpn().
Also we use the svk params in a case statement to decipher the
hexstream better.
The hexstream ($line) has now no blanks anymore. They seem to be
arbitrary.
Variables need to be declared in get_https_rrecord() .
---
testssl.sh | 75 +++++++++++++++++++++++++++++++++++++-----------------
1 file changed, 51 insertions(+), 24 deletions(-)
diff --git a/testssl.sh b/testssl.sh
index 2476b32..4c52bbd 100755
--- a/testssl.sh
+++ b/testssl.sh
@@ -22726,6 +22726,9 @@ get_https_rrecord() {
# 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
+
+local text=""
+
if [[ -z "$raw_https" ]]; then
return 1
elif [[ "$raw_https" =~ \#\ [0-9][0-9] ]]; then
@@ -22733,45 +22736,69 @@ get_https_rrecord() {
read hash len line <<< "$raw_https"
# \# 10 00010000010003026832
# \# 36 000100000100030268320003000201BB000600102A01023842816755 10000000000B1337
- [[ $DEBUG -eq 1 ]] && echo "$hash $len $line"
+ len=${len// /} # remove spaces
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
# mock text representation
- svc_priority+=" . "
- alpnID="${alpnID}${svc_priority}"
+ svc_priority+=" . " #FIXME? needs more testing
+ text="${text}${svc_priority}"
fi
- 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
- fi
- 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+="\"" # if alpn and we're done add trailing double quote
-
- # 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_entry=${line:12:2}
+ len_entry=$(( ((10#$len_entry)) * 2 )) # make sure we count in the right system
+ entry=${line:14:$len_entry}
+ # Service Parameter Keys https://www.rfc-editor.org/info/rfc9460/#name-initial-contents
+ case ${line:8:2} in
+ 00) # "mandatory"
+ ;;
+ 01) # "alpn"
+ text+=$(decode_https_rr_alpn $entry) ;;
+ 02) # "no-default-alpn"
+ ;;
+ 03) # "port"
+ ;;
+ 04) # "ipv4hint"
+ ;;
+ 05) # "ech"
+ ;;
+ 06) # "ipv6hint"
+ ;;
+ esac
else
out "please report unknown HTTPS RR $line with flag @ $NODE"
return 7
fi
- safe_echo "$alpnID"
+ safe_echo "$text"
+ [[ $DEBUG -eq 1 ]] && echo "$hash $len $line" >&2
+ [[ $DEBUG -eq 1 ]] && echo "key: ${line:localPTR:4}" >&2
fi
return 0
}
+decode_https_rr_alpn() {
+ local entry="$1"
+ local -i len="${#entry}"
+ local -i ptr=0
+ local alpn_wire="" str=""
+ local alpn_len=""
+
+ ptr=0
+ while (( ptr < len )); do
+ [[ -n "$alpn_str" ]] && alpn_str+="," # add a comma in the >=2 round
+ alpn_len=${entry:$ptr:2}
+ alpn_len=$(( ((10#$alpn_len)) * 2 )) # conversion, make sure it's the right format
+
+ ptr=$((ptr + 2)) # len field is always 2 bytes
+ alpn_wire=${entry:$ptr:$alpn_len}
+ str=$(hex2ascii $alpn_wire)
+ ptr=$((ptr + alpn_len))
+ alpn_str+="$str"
+ done
+ safe_echo "alpn=\"$alpn_str\""
+}
+
# arg1: domain to check for. Returned will be the MX record as a string
get_mx_record() {
From 37135fa75251db6b66417d470e6f2b617aa13a07 Mon Sep 17 00:00:00 2001
From: Dirk Wetter
Date: Tue, 9 Jun 2026 22:14:45 +0200
Subject: [PATCH 07/13] Save work
- dig needs to be called with $DIG_R
- basic parsing for alpn on Mac should be fine now
- case statement filled with moste of the functions
- port function tested + added, but not called yet
- ipv4hint function tested + added but not called yet
- ipv6hint function tested + added but not called yet. Doesn't do compression of ipv6 address yet
- stub functions dohpath+ech
---
testssl.sh | 142 ++++++++++++++++++++++++++++++++++++++++++++++-------
1 file changed, 125 insertions(+), 17 deletions(-)
diff --git a/testssl.sh b/testssl.sh
index 556dd3e..125ceec 100755
--- a/testssl.sh
+++ b/testssl.sh
@@ -22393,8 +22393,10 @@ check_resolver_bins() {
# Pre-checking the following for HTTPS RR, see get_https_rrecord()
if "$HAS_DIG"; then
- str=$(dig +short $testhost HTTPS)
- if [[ -z "$str" ]] && [[ ! "$str" =~ 127.0.0.1 ]] ; then
+ str=$(dig $DIG_R +short $testhost HTTPS)
+ if [[ -z "$str" ]] && [[ ! "$str" =~ 127.0.0.1 ]] && \
+ # MacOS runners are problematic otherwise:
+ dig $DIG_R +nocomments $testhost HTTPS | grep -q 'IN.*HTTPS'; then
HAS_DIG_HTTPS=true
fi
elif "$HAS_DRILL"; then
@@ -22642,7 +22644,7 @@ get_https_rrecord() {
# "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 | tail -1)"
+ text_httpsrr="$(dig $DIG_R +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 | tail -1)"
elif "$HAS_HOST_HTTPS"; then
@@ -22707,7 +22709,7 @@ get_https_rrecord() {
# 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
+ # TL alpn| L h 2 |=2 round
alpn_len=${entry:$ptr:2}
alpn_len=$(( ((10#$alpn_len)) * 2 )) # conversion, make sure it's the right format
- ptr=$((ptr + 2)) # len field is always 2 bytes
+ ptr=$((ptr + 2)) # len field is always 2 bytes
alpn_wire=${entry:$ptr:$alpn_len}
str=$(hex2ascii $alpn_wire)
ptr=$((ptr + alpn_len))
@@ -22798,6 +22803,109 @@ decode_https_rr_alpn() {
safe_echo "alpn=\"$alpn_str\""
}
+# key 3 — port: single u16 override port
+#
+decode_https_rr_port() {
+ local entry="$1"
+ local -i len="${#entry}"
+ local -i ptr=2
+ local port_wire="" str=""
+
+ # we assume it's one port only and it starts at $ptr and is $len-$ptr long
+ port_wire=${entry:$ptr:$((len - ptr))}
+ str=$((16#$port_wire)) # hex2dec
+ port_str+="$str"
+ safe_echo "port=\"$port_str\""
+}
+
+# key 4 — ipv4hint: one or more 4-byte IPv4 addresse
+#
+decode_https_rr_ipv4() {
+ local entry="$1"
+ local -i len="${#entry}"
+ local -i ptr=2
+ local ipv4_wire="" str=""
+ # we currently don't need that:
+ # local nr_ips="${1:0:2}"
+
+ while (( ptr < len )); do
+ ipv4_wire=${entry:$ptr:2}
+ str=$((16#$ipv4_wire)) # hex2dec
+ ipv4_str+="$str"
+
+ # if the end is not reached yet
+ # after address 2,4,6, 10,12,14, ... we need a dot
+ # after address 18, 16, ... we need a comma
+
+ if [[ $len -ne $((ptr + 2)) ]]; then
+ if [[ $((ptr % 8 )) -eq 0 ]] ; then
+ ipv4_str+=","
+ else
+ ipv4_str+="."
+ fi
+ fi
+ ptr=$((ptr + 2)) # two bytes per octet
+ done
+ safe_echo "ipv4hint=\"$ipv4_str\""
+}
+
+
+# key 5 — ech: opaque ECHConfigList blob, show as truncated hex
+#
+decode_https_rr_ech() {
+ echo
+}
+
+
+# key 6 — ipv6hint: one or more 16-byte IPv6 addresses
+#FIXME: doesn't do IPv6 compression yet
+decode_https_rr_ipv6() {
+ local entry="$1"
+ local -i len="${#entry}"
+ local -i ptr=2 # we start at pos 2
+ local ipv6_wire="" str=""
+ # local nr_ips="${1:0:2}"
+
+ while (( ptr < len )); do
+ ipv6_wire=${entry:$ptr:4}
+ ipv6_str+="$ipv6_wire"
+
+ # We have 8 octets filled with zero if needed --> 32 chars
+
+ if [[ $len -ne $((ptr + 4)) ]]; then
+ if [[ $((ptr % 30 )) -eq 0 ]] ; then # we have two bytes pointer 30+2=32
+ ipv6_str+=","
+ else
+ ipv6_str+=":"
+ fi
+ fi
+ ptr=$((ptr + 4)) # two byte per octett
+ done
+
+ ipv6_str="$(tolower "$ipv6_str")"
+
+ safe_echo "ipv6hint=\"$ipv6_str\""
+}
+
+
+# key 7 — dohpath: UTF-8 URI template for DNS-over-HTTPS
+#FIXME --> to test!
+#
+decode_dohpath() {
+ local entry="$1"
+ local -i len="${#entry}"
+ # local len=$1
+ local path=""
+ local -i i
+
+ for (( i = 0; i < len; i++ )); do
+ path+=$(printf "\\$(printf '%03o' "${PARAM_VALUE_BYTES[$i]}")")
+ done
+ safe_echo "$path"
+}
+
+
+
# arg1: domain to check for. Returned will be the MX record as a string
get_mx_record() {
From 913bf1406dc40fdab72836db2f7bb1d087678ac3 Mon Sep 17 00:00:00 2001
From: Dirk Wetter
Date: Tue, 16 Jun 2026 11:00:14 +0200
Subject: [PATCH 08/13] Save work
- parsing output from old dig versions (Mac) works for almost every svc_key
- for old dig versions: double lined RR work (but output is not nice yet)
- cleaned up comments
- separate function https_rr_raw_parser()
- commented output from claude.ai for ech for later interpretation
- get_mx_record() has a warning when get_https_rrecord returned != 0
---
testssl.sh | 258 +++++++++++++++++++++++++++++++----------------------
1 file changed, 150 insertions(+), 108 deletions(-)
diff --git a/testssl.sh b/testssl.sh
index 125ceec..db2844b 100755
--- a/testssl.sh
+++ b/testssl.sh
@@ -22623,15 +22623,10 @@ get_caa_rrecord() {
#
get_https_rrecord() {
local raw_https=""
- local hash="" line=""
- local len # better fix as integer?
- local len_alpnID=""
- local alpnID=""
- local alpnID_wire=""
+ local line=""
local saved_openssl_conf="$OPENSSL_CONF"
local all_https=""
local noidnout=""
- local svc_priority=""
[[ -n "$NODNS" ]] && return 2 # if minimum DNS lookup was instructed, leave here
"$HAS_DIG_NOIDNOUT" && noidnout="+noidnout"
@@ -22642,6 +22637,8 @@ get_https_rrecord() {
# 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
+ #FIXME: likely causes a problem with mulitline RR
+
OPENSSL_CONF=""
if "$HAS_DIG_HTTPS"; then
text_httpsrr="$(dig $DIG_R +short +search +timeout=3 +tries=3 $noidnout HTTPS "$1" 2>/dev/null | tail -1)"
@@ -22702,80 +22699,100 @@ get_https_rrecord() {
fi
OPENSSL_CONF="$saved_openssl_conf" # We're done now with openssl, see https://github.com/drwetter/testssl.sh/issues/134
- # 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)
-
- # 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 | 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
-
- # 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
-
-local text=""
-
if [[ -z "$raw_https" ]]; then
return 1
- elif [[ "$raw_https" =~ \#\ [0-9][0-9] ]]; then
- # signals that we're on the right track with type65 interpretation
- read hash len line <<< "$raw_https"
- # \# 10 00010000010003026832
- # \# 36 000100000100030268320003000201BB000600102A01023842816755 10000000000B1337
+ fi
- len=${len// /} # remove spaces
- 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
- # mock text representation
- svc_priority+=" . " #FIXME? needs more testing
- text="${text}${svc_priority}"
+ # Now comes the third, tricky part (old dig for Macs e.g.) --> parsing the hex stream which was returned if it was returned.
+ # https_rr_raw_parser() takes care of that. Format is like: https://www.rfc-editor.org/rfc/rfc3597 (plus updates)
+
+ local -i i=0
+ while IFS= read -r line; do
+ https_rr_raw_parser "$line" || return 1
+ ((i++))
+ # in rare cases there can be two lines (sodiao.cc) or more, #FIXME: output formatting is wrong
+ [[ "$raw_https" == *$'\n'* ]] && [[ $i -ge 1 ]] && outln
+ done <<< "$raw_https"
+}
+
+
+https_rr_raw_parser () {
+ local raw_https="$1"
+ local hash="" line=""
+ local len="" len_next_entry=""
+ local svc_priority="" svc_key=""
+ local text=""
+ local -i ptr=0
+ local first=true
+
+ if [[ "$raw_https" =~ \#\ [0-9][0-9] ]]; then # check we're on the right track with type65 interpretation
+ read hash len line <<< "$raw_https"
+
+ # testssl.sh \# 10 00010000010003026832 --> 1. alpn="h2"
+ # dev.testssl.sh \# 36 000100000100030268320003000201BB000600102A01023842816755 10000000000B1337 ----> 1. alpn="h2" port=443 ipv6hint=2a01:238:4281:6755:1000:0:b:1337
+ # google.com \# 13 00010000010006026832026833 --> 1. alpn="h2,h3"
+ # b-cdn.net \# 27 0001000001000C02683208687474702F312E3100040004A996F722 --> alpn="h2,http/1.1" ipv4hint=169,150.247.34
+ # testssl.net \# 136 00010000010006026833026832000400086815229AAC43CDE7000500 470045FE0D0041F3002000202BD0935ED66980C1862F2570C0D6014D 733A7CFAAEA5E4DD9CA43D4C24199E330004000100010012636C6F75 64666C6172652D6563682E636F6D0000000600202606470030310000 00000000AC43CDE72606470030360000000000006815229A
+ # --> 1. alpn="h3,h2" ipv4hint=104.21.34.154,172.67.205.231 ech=AEX+DQBB1gAgACDasOut8j3EAZ6Rc04Wy0Vm+fj/SiHZWUZIeH3bRtoyAQAEAAEAAQASY2xvdWRmbGFyZS1lY2guY29tAAA= ipv6hint=2606:4700:3031::ac43:cde7,2606:4700:3036::6815:229a
+ # more @ https://github.com/yzzhn/imc2024dnshttps
+
+ line=${line// /} # remove spaces
+ if [[ $((len * 2)) -ne ${#line} ]]; then # again a consistency check
+ echo "inconsistent length for type65 hex stream parsing"
+ return 1
+ fi
+ if [[ "${line:0:4}" =~ ^(0001|0002)$ ]]; then # marker to proceed, belongs to SvcPriority, see rfc9460, 2.4.3
+ svc_priority=$(printf "%0d" "$((10#${line:2:2}))") # 1 is most often, 2 is possible, 0 is alias (to be tested)
+ if [[ $svc_priority =~ ^(1|2)$ ]]; then
+ # mock text representation
+ svc_priority+=" . " #FIXME: what about 0?
+ text="${text}${svc_priority}"
+ ptr=6 # This is at the start
+ fi
+ while (( ptr < ${#line} )); do
+ if "$first"; then
+ first=false
+ else
+ text+=" "
fi
- len_entry=${line:12:2}
- len_entry=$(( ((10#$len_entry)) * 2 )) # make sure we count in the right system
- entry=${line:14:$len_entry}
+ ptr=$(( ptr + 2 ))
+ svc_key=${line:$ptr:2}
+ ptr=$(( ptr + 4 ))
+
+ len_next_entry=${line:$ptr:2}
+ len_next_entry=$((16#${len_next_entry})) # it's a hex number
+ len_next_entry=$((len_next_entry * 2 ))
+ ptr=$(( ptr + 2 ))
+ entry=${line:$ptr:$len_next_entry}
+
+ debugme echo "-- $svc_key : $entry ($len_next_entry) --"
# Service Parameter Keys https://www.rfc-editor.org/info/rfc9460/#name-initial-contents
- case ${line:8:2} in
+ case $svc_key in
00) # = "mandatory", skipping that
;;
- 01) # = "alpn"
- text+=$(decode_https_rr_alpn $entry) ;;
- 02) # = "no-default-alpn", skipping that
+ 01) text+=$(decode_https_rr_alpn $entry)
;;
- 03) # = "port"
- text+=$(decode_https_rr_port $entry) ;;
- 04) # = "ipv4hint"
- text+=$(decode_https_rr_ipv4hint $entry) ;;
- 05) # = "ech"
- text+=$(decode_https_rr_ech $entry) ;;
- 06) # = "ipv6hint"
- text+=$(decode_https_rr_ipv6hint $entry) ;;
- 07) # = "dohpath"
- text+=$(decode_https_rr_dohpath $entry) ;;
- esac
- else
- out "please report unknown HTTPS RR $line with flag @ $NODE"
- return 7
- fi
- safe_echo "$text"
- [[ $DEBUG -eq 1 ]] && echo "$hash $len $line" >&2
- [[ $DEBUG -eq 1 ]] && echo "key: ${line:localPTR:4}" >&2
+ 02) text+="no-default-alpn"
+ ;;
+ 03) text+=$(decode_https_rr_port $entry)
+ ;;
+ 04) text+=$(decode_https_rr_ipv4 $entry)
+ ;;
+ 05) text+=$(decode_https_rr_ech $entry)
+ ;;
+ 06) text+=$(decode_https_rr_ipv6 $entry)
+ ;;
+ 07) text+=$(decode_https_rr_dohpath $entry)
+ ;;
+ esac
+ ptr=$((10#${#entry} + ptr ))
+ done
+ else
+ safe_echo "please report unknown HTTPS RR $line from $NODE"
+ return 1
+ fi
+ safe_echo "$text"
fi
return 0
}
@@ -22792,7 +22809,7 @@ decode_https_rr_alpn() {
while (( ptr < len )); do
[[ -n "$alpn_str" ]] && alpn_str+="," # add a comma in the >=2 round
alpn_len=${entry:$ptr:2}
- alpn_len=$(( ((10#$alpn_len)) * 2 )) # conversion, make sure it's the right format
+ alpn_len=$(( ((10#$alpn_len)) * 2 )) # also make sure it's a number
ptr=$((ptr + 2)) # len field is always 2 bytes
alpn_wire=${entry:$ptr:$alpn_len}
@@ -22803,30 +22820,27 @@ decode_https_rr_alpn() {
safe_echo "alpn=\"$alpn_str\""
}
-# key 3 — port: single u16 override port
+# key 3 — port: single one
#
decode_https_rr_port() {
local entry="$1"
local -i len="${#entry}"
- local -i ptr=2
local port_wire="" str=""
- # we assume it's one port only and it starts at $ptr and is $len-$ptr long
- port_wire=${entry:$ptr:$((len - ptr))}
- str=$((16#$port_wire)) # hex2dec
+ # we assume it's one port only and it starts at $ptr and is $len long
+ port_wire=${entry:0:$len} # we start @ pos=0 and assume, it's one port only, otherwise we need to extend this, see ipv6 func e.g.
+ str=$((16#$port_wire)) # hex2dec. Works too: printf "%d\n" "0x$port_wire"
port_str+="$str"
- safe_echo "port=\"$port_str\""
+ safe_echo "port=${port_str}"
}
-# key 4 — ipv4hint: one or more 4-byte IPv4 addresse
+# key 4 — ipv4hint: one or more 4-byte IPv4 addresses
#
decode_https_rr_ipv4() {
local entry="$1"
local -i len="${#entry}"
- local -i ptr=2
+ local -i ptr=0 # we start @ pos=0
local ipv4_wire="" str=""
- # we currently don't need that:
- # local nr_ips="${1:0:2}"
while (( ptr < len )); do
ipv4_wire=${entry:$ptr:2}
@@ -22838,7 +22852,7 @@ decode_https_rr_ipv4() {
# after address 18, 16, ... we need a comma
if [[ $len -ne $((ptr + 2)) ]]; then
- if [[ $((ptr % 8 )) -eq 0 ]] ; then
+ if [[ $(( ((ptr + 2 )) % 8 )) -eq 0 ]] ; then
ipv4_str+=","
else
ipv4_str+="."
@@ -22846,67 +22860,92 @@ decode_https_rr_ipv4() {
fi
ptr=$((ptr + 2)) # two bytes per octet
done
- safe_echo "ipv4hint=\"$ipv4_str\""
+ safe_echo "ipv4hint=${ipv4_str}"
}
-# key 5 — ech: opaque ECHConfigList blob, show as truncated hex
+# key 5 — encrypted client hello: pub key and more
#
decode_https_rr_ech() {
- echo
+ # cloudflare-ech.com (base64 format conversion between the two):
+ # text format: AEX+DQBB+QAgACD4885ZLoES1IllBXr15/nI6vPXjTcxfiM02O8nxfZgXwAEAAEAAQASY2xvdWRmbGFyZS1lY2guY29tAAA=
+ # wire format: 0045FE0D0041F900200020F8F3CE592E8112D48965057AF5E7F9C8EAF3D78D37317E2334D8EF27C5F6605F0004000100010012636C6F7564666C6172652D6563682E636F6D0000
+
+# interpretation from claude.ai, to be double checked:
+# 00 45 ECHConfigList.length = 0x0045 = 69
+# FE 0D ECHConfig.version = 0xfe0d (ECH draft-13)
+# 00 41 ECHConfig.length = 0x0041 = 65
+# F9 config_id = 0xF9 (249)
+# 00 20 kem_id = 0x0020 = DHKEM(X25519, HKDF-SHA256)
+# 00 20 public_key_len = 32
+# F8 F3 CE 59 2E 81 12 D4
+# 89 65 05 7A F5 E7 F9 C8
+# EA F3 D7 8D 37 31 7E 23
+# 34 D8 EF 27 C5 F6 60 5F public_key (32 bytes, X25519 pubkey)
+# 00 04 cipher_suites_len = 4
+# 00 01 00 01 one suite: KDF=0x0001 (HKDF-SHA256), AEAD=0x0001 (AES-128-GCM)
+# 00 maximum_name_length = 0
+# 12 public_name_len = 18
+# 63 6C 6F 75 64 66 6C 61
+# 72 65 2D 65 63 68 2E 63
+# 6F 6D public_name = "cloudflare-ech.com"
+# 00 00 extensions_len = 0
+
+ # for now we just encode the wire format to the base64 format
+ safe_echo "ech=$(hex2ascii "$1" | $OPENSSL base64 -A 2>/dev/null)"
}
-
# key 6 — ipv6hint: one or more 16-byte IPv6 addresses
-#FIXME: doesn't do IPv6 compression yet
+#
decode_https_rr_ipv6() {
local entry="$1"
local -i len="${#entry}"
- local -i ptr=2 # we start at pos 2
local ipv6_wire="" str=""
- # local nr_ips="${1:0:2}"
+ local -i ptr=0 # we start @ pos=0
while (( ptr < len )); do
- ipv6_wire=${entry:$ptr:4}
+ ipv6_wire=${entry:$ptr:4} # we have 8 hextets, length 4, filled with zero if needed --> 32 chars
ipv6_str+="$ipv6_wire"
- # We have 8 octets filled with zero if needed --> 32 chars
-
if [[ $len -ne $((ptr + 4)) ]]; then
- if [[ $((ptr % 30 )) -eq 0 ]] ; then # we have two bytes pointer 30+2=32
+ if [[ $(( ((ptr + 4)) % 32 )) -eq 0 ]]; then # we have two bytes pointer 30+2=32
ipv6_str+=","
else
ipv6_str+=":"
fi
fi
- ptr=$((ptr + 4)) # two byte per octett
+ ptr=$((ptr + 4)) # two byte per hextets
done
ipv6_str="$(tolower "$ipv6_str")"
- safe_echo "ipv6hint=\"$ipv6_str\""
+ # poor man's compression, max 5 zero hextets
+ ipv6_str=${ipv6_str//:0000:0000:0000:0000:0000:/::}
+ ipv6_str=${ipv6_str//:0000:0000:0000:0000:/::}
+ ipv6_str=${ipv6_str//:0000:0000:0000:/::}
+ ipv6_str=${ipv6_str//:0000:0000:/::}
+ ipv6_str=${ipv6_str//:0000:/::}
+
+ # strip up to 3 leading zeros in a hextet
+ ipv6_str=${ipv6_str//:0/:}
+ ipv6_str=${ipv6_str//:0/:}
+ ipv6_str=${ipv6_str//:0/:}
+
+ safe_echo "ipv6hint=${ipv6_str}"
}
-
# key 7 — dohpath: UTF-8 URI template for DNS-over-HTTPS
-#FIXME --> to test!
+#FIXME: likely doesn't work, not tested
#
decode_dohpath() {
local entry="$1"
local -i len="${#entry}"
- # local len=$1
- local path=""
- local -i i
+ local path=$( hex2ascii "$1" )
- for (( i = 0; i < len; i++ )); do
- path+=$(printf "\\$(printf '%03o' "${PARAM_VALUE_BYTES[$i]}")")
- done
- safe_echo "$path"
+ safe_echo "$path (please report this @ github)"
}
-
-
# arg1: domain to check for. Returned will be the MX record as a string
get_mx_record() {
local mx=""
@@ -23748,7 +23787,10 @@ dns_https_rr () {
# 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
+ if [[ $? -ne 0 ]]; then
+ prln_warning "$https_rr"
+ fileout "${jsonID}" "WARN" "$https_rr"
+ elif [[ -n "$https_rr" ]]; then
pr_svrty_good "yes" ; out ": "
prln_italic "$(out_row_aligned_max_width "$https_rr" "$indent " $TERM_WIDTH)"
fileout "${jsonID}" "OK" "$https_rr"
From 1f9e61afbcaa05c5323f351f71f86e42faede4a7 Mon Sep 17 00:00:00 2001
From: Dirk Wetter
Date: Tue, 16 Jun 2026 13:15:11 +0200
Subject: [PATCH 09/13] Fix CI runner for Mac
---
testssl.sh | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/testssl.sh b/testssl.sh
index db2844b..e902977 100755
--- a/testssl.sh
+++ b/testssl.sh
@@ -22707,12 +22707,16 @@ get_https_rrecord() {
# https_rr_raw_parser() takes care of that. Format is like: https://www.rfc-editor.org/rfc/rfc3597 (plus updates)
local -i i=0
+ local -i nr_lines=$(grep -c '^' <<< "$raw_https")
+ # In rare cases there can be two lines (sodiao.cc) or more
while IFS= read -r line; do
https_rr_raw_parser "$line" || return 1
+ [[ $nr_lines -eq 1 ]] && break # return here for a one liner, otherwise next time we hit return 1
((i++))
- # in rare cases there can be two lines (sodiao.cc) or more, #FIXME: output formatting is wrong
- [[ "$raw_https" == *$'\n'* ]] && [[ $i -ge 1 ]] && outln
+ [[ $i -eq $nr_lines ]] && break # we hit the last line
+ [[ $i -ge 1 ]] && out " / " # hack: two lines are merged into one output line and separated by "/"
done <<< "$raw_https"
+ return 0
}
From 7e97b243d12b5c190afa4f4f2b0b1d5dcc8deb79 Mon Sep 17 00:00:00 2001
From: Dirk Wetter
Date: Mon, 22 Jun 2026 14:59:13 +0200
Subject: [PATCH 10/13] Introduce global HTTPS_RR variable
... which is initialized with "initt" to distinguish between not being tested yet and no value.
We only display the value once per $NODE for the first IP address being tested.
HTTPS_RR doesn't have to be reset in reset_hostdepended_vars()
Few comments were added / indentation fixed (not relevant to this PR)
---
testssl.sh | 28 ++++++++++++++++++----------
1 file changed, 18 insertions(+), 10 deletions(-)
diff --git a/testssl.sh b/testssl.sh
index e902977..392ca63 100755
--- a/testssl.sh
+++ b/testssl.sh
@@ -248,7 +248,7 @@ CIPHERS_BY_STRENGTH_FILE=""
TLS_DATA_FILE="" # mandatory file for socket-based handshakes
OPENSSL="" # ~/bin/openssl.$(uname).$(uname -m) if you run this from GitHub. Linux otherwise probably /usr/bin/openssl
OPENSSL2=${OPENSSL2:-/usr/bin/openssl} # This will be openssl version >=1.1.1 (auto determined) as opposed to openssl-bad (OPENSSL)
-HAS2_TLS13=false # If we run with supplied binary AND $OPENSSL2 supports TLS 1.3 this will be set to true
+HAS2_TLS13=false # If we run with supplied binary AND $OPENSSL2 supports TLS 1.3 this will be set to true
HAS2_CHACHA20=false
HAS2_AES128_GCM=false
HAS2_AES256_GCM=false
@@ -377,7 +377,7 @@ HAS_UDS=false
HAS2_UDS=false
HAS_ENABLE_PHA=false
HAS_DIG=false
-HAS_DIG_R=true
+HAS_DIG_R=true # Variable for "do not read ~/.digrc"
DIG_R="-r"
HAS_HOST=false
HAS_DRILL=false
@@ -404,6 +404,7 @@ IPADDRs2CHECK="" # Contains all IP addresses to test
IPADDRs2SHOW="" # ... those are the ones to be displayed
LOCAL_A=false # Does the $NODEIP come from /etc/hosts?
LOCAL_AAAA=false # Does the IPv6 IP come from /etc/hosts?
+HTTPS_RR="init" # Keeps the HTTPS RR record. That is per $NODE/NODEIP identical. "init" signals not being tested yet
XMPP_HOST=""
PROXYIP="" # $PROXYIP:$PROXPORT is your proxy if --proxy is defined ...
PROXYPORT="" # ... and openssl has proxy support
@@ -2572,6 +2573,7 @@ service_detection() {
out " not identified, but mTLS authentication is set ==> trying HTTP checks"
SERVICE=HTTP
fileout "${jsonID}" "DEBUG" "Couldn't determine service -- ASSUME_HTTP set"
+ dns_https_rr
elif [[ "$CLIENT_AUTH" == required ]] && [[ -z $MTLS ]]; then
out " certificate-based authentication without providing client certificate and private key => skipping all HTTP checks" | tee $TMPFILE
fileout "${jsonID}" "INFO" "certificate-based authentication without providing client certificate and private key => skipping all HTTP checks"
@@ -2581,6 +2583,7 @@ service_detection() {
SERVICE=HTTP
out " -- ASSUME_HTTP set though"
fileout "${jsonID}" "DEBUG" "Couldn't determine service -- ASSUME_HTTP set"
+ dns_https_rr
else
out ", assuming no HTTP service => skipping all HTTP checks"
fileout "${jsonID}" "DEBUG" "Couldn't determine service, skipping all HTTP checks"
@@ -23773,13 +23776,18 @@ determine_optimal_proto() {
return 0
}
-
+# High level function of getting the DNS HTTP RR and outputting them. The global variable
+# HTTPS_RR is initialized with "reset" to distinguish between not being tested yet and no value.
+# HTTPS_RR doesn't have to be reset in reset_hostdepended_vars()
+#
dns_https_rr () {
local jsonID="DNS_HTTPS_rrecord"
- local https_rr=""
local indent=""
local https_rr_node="$NODE"
+ # see comment above. We only display the RR 1x per $NODE
+ [[ "$HTTPS_RR" != init ]] && return 0
+
out "$indent"; pr_bold " DNS HTTPS RR"; out " (expt.): "
if [[ -n "$NODNS" ]]; then
out "(instructed to minimize/skip DNS queries)"
@@ -23790,14 +23798,14 @@ dns_https_rr () {
else
# append a dot if there was none
[[ $https_rr_node =~ '.'$ ]] || https_rr_node+="."
- https_rr="$(get_https_rrecord $https_rr_node)"
+ HTTPS_RR="$(get_https_rrecord $https_rr_node)"
if [[ $? -ne 0 ]]; then
- prln_warning "$https_rr"
- fileout "${jsonID}" "WARN" "$https_rr"
- elif [[ -n "$https_rr" ]]; then
+ prln_warning "$HTTPS_RR"
+ fileout "${jsonID}" "WARN" "$HTTPS_RR"
+ elif [[ -n "$HTTPS_RR" ]]; then
pr_svrty_good "yes" ; out ": "
- prln_italic "$(out_row_aligned_max_width "$https_rr" "$indent " $TERM_WIDTH)"
- fileout "${jsonID}" "OK" "$https_rr"
+ prln_italic "$(out_row_aligned_max_width "$HTTPS_RR" "$indent " $TERM_WIDTH)"
+ fileout "${jsonID}" "OK" "$HTTPS_RR"
else
outln "--"
fileout "${jsonID}" "INFO" " no resource record found"
From dca6434604be70a13bcfc53ae3028a424d932e78 Mon Sep 17 00:00:00 2001
From: Dirk Wetter
Date: Mon, 22 Jun 2026 16:20:13 +0200
Subject: [PATCH 11/13] Compare QUIC section with DNS HTTPS RR
Also: make "A(AAA) record via:" bold, to be in line with the other keys
---
testssl.sh | 52 ++++++++++++++++++++++++++++++++++++++++++----------
1 file changed, 42 insertions(+), 10 deletions(-)
diff --git a/testssl.sh b/testssl.sh
index 392ca63..48b3c6a 100755
--- a/testssl.sh
+++ b/testssl.sh
@@ -6293,12 +6293,17 @@ sub_quic() {
local sclient_outfile="$TEMPDIR/$NODEIP.quic_connect.txt"
local sclient_errfile="$TEMPDIR/$NODEIP.quic_connect_err.txt"
local jsonID="QUIC"
+ local has_https_rr_h3=false
[[ $DEBUG -ne 0 ]] && sclient_errfile=/dev/null
[[ "$SERVICE" != HTTP ]] && return 0
pr_bold " QUIC ";
+ if [[ "$HTTPS_RR" == *"h3"* ]]; then
+ has_https_rr_h3=true
+ fi
+
if "$HAS2_QUIC" || "$HAS_QUIC"; then
# Proxying QUIC seems not supported
# The s_client call would block if either the remote side doesn't support QUIC or outbound traffic is blocked
@@ -6307,6 +6312,12 @@ sub_quic() {
else
use_openssl="$OPENSSL"
fi
+ if "$has_https_rr_h3"; then
+ if [[ $QUIC_WAIT -eq 3 ]]; then
+ # change the default for QUIC testing to be a bit more conservative --unless not default value wasn't changed
+ QUIC_WAIT=5
+ fi
+ fi
OPENSSL_CONF='' $use_openssl s_client -quic -alpn h3 -connect $NODEIP:$PORT -servername $NODE $sclient_errfile >$sclient_outfile &
wait_kill $! $((QUIC_WAIT * 10))
@@ -6322,17 +6333,34 @@ sub_quic() {
# 0 would be process terminated before be killed. Now find out what happened...
filter_printable $sclient_outfile
if [[ $(< $sclient_outfile) =~ CERTIFICATE----- ]]; then
+ "$has_https_rr_h3" || \
+ fileout "$jsonID" "OK" "offered" && \
+ fileout "$jsonID" "OK" "offered, as advertised in DNS HTTPS RR"
pr_svrty_best "offered (OK)"
- fileout "$jsonID" "OK" "offered"
alpn="$(awk -F':' '/^ALPN protocol/ { print $2 }' < $sclient_outfile)"
alpn="$(strip_spaces $alpn)"
- outln ": $(awk '/^Protocol:/ { print $2 }' 2>/dev/null < $sclient_outfile) ($alpn)"
+ out ": $(awk '/^Protocol:/ { print $2 }' 2>/dev/null < $sclient_outfile) ($alpn)"
+ "$has_https_rr_h3" && \
+ out ", as advertised in DNS HTTPS RR"
+ outln
elif [[ $(< $sclient_outfile) =~ ^CONNECTED\( ]]; then
- outln "not offered (but UDP connection succeeded)"
- fileout "$jsonID" "INFO" "not offered (but UDP connection succeeded)"
+ if [[ "$has_https_rr_h3" ]]; then
+ out "not offered (but UDP connection succeeded), "
+ prln_svrty_low "double check wrt HTTPS DNS RR entry"
+ fileout "$jsonID" "LOW" "not offered (but UDP connection succeeded) but contradicts HTTPS DNS RR entry"
+ else
+ outln "not offered (but UDP connection succeeded)"
+ fileout "$jsonID" "INFO" "not offered (but UDP connection succeeded)"
+ fi
else
- outln "not offered"
- fileout "$jsonID" "INFO" "not offered"
+ if [[ "$has_https_rr_h3" ]]; then
+ out "not offered, "
+ prln_svrty_low "double check wrt HTTPS DNS RR entry"
+ fileout "$jsonID" "INFO" "not offered but contradicts HTTPS DNS RR entry"
+ else
+ outln "not offered"
+ fileout "$jsonID" "INFO" "not offered"
+ fi
fi
fi
else
@@ -24027,14 +24055,18 @@ display_rdns_etc() {
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 "
+ pr_bold " A record via:"
+ outln " $CORRECT_SPACES /etc/hosts "
elif "$LOCAL_AAAA"; then
- outln " AAAA record via: $CORRECT_SPACES /etc/hosts "
+ pr_bold " AAAA record via:"
+ outln " $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\""
+ pr_bold " AAAA record via:"
+ outln " $CORRECT_SPACES supplied IP \"$CMDLINE_IP\""
else
- outln " A record via: $CORRECT_SPACES supplied IP \"$CMDLINE_IP\""
+ pr_bold " A record via:"
+ outln " $CORRECT_SPACES supplied IP \"$CMDLINE_IP\""
fi
fi
pr_bold " rDNS "
From 859d24df2080c98202215d0a009a21310ec7f0c9 Mon Sep 17 00:00:00 2001
From: Dirk Wetter
Date: Mon, 22 Jun 2026 16:37:32 +0200
Subject: [PATCH 12/13] HTTPS DNS RR in manual
---
doc/testssl.1.md | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/doc/testssl.1.md b/doc/testssl.1.md
index db58e8e..6470786 100644
--- a/doc/testssl.1.md
+++ b/doc/testssl.1.md
@@ -36,9 +36,9 @@ linked OpenSSL binaries for major operating systems are supplied in `./bin/`.
`testssl.sh URI` as the default invocation does the so-called default run which does a number of checks and puts out the results colorized (ANSI and termcap) on the screen. It does every check listed below except `-E` which are (order of appearance):
-0) displays a banner (see below), does a DNS lookup also for further IP addresses and does for the returned IP address a reverse lookup. Last but not least a service check is being done.
+0) displays a banner (see below), does a DNS lookup also for further IP addresses and does for the returned IP address a reverse lookup. Also the so called DNS HTTPS record is being queried and displayed (for the first IP only). Last but not least a service check is being done.
-1) SSL/TLS protocol check
+1) SSL/TLS protocol check plus QUIC and ALPN check
2) standard cipher categories
@@ -133,7 +133,7 @@ The same can be achieved by setting the environment variable `WARNINGS`.
`-4` scans only IPv4 addresses of the target, IPv6 addresses of the target won't be scanned.
-`--ssl-native` Instead of using a mixture of bash sockets and a few openssl s_client connects, testssl.sh uses the latter (almost) only. This is faster but provides less accurate results, especially for the client simulation and for cipher support. For all checks you will see a warning if testssl.sh cannot tell if a particular check cannot be performed. For some checks however you might end up getting false negatives without a warning. Thus it is not recommended to use. It should only be used if you prefer speed over accuracy or you know that your target has sufficient overlap with the protocols and cipher provided by your openssl binary.
+`--ssl-native` Instead of using a mixture of bash sockets and a few `openssl s_client connect`s, testssl.sh uses the latter (almost) only. This is faster but doesn't provides accurate results, especially for the client simulation and for cipher support. Thus this is not recommended anymore. For all checks you will see a warning if testssl.sh cannot tell if a particular check cannot be performed. For some checks however you might end up getting false negatives without a warning. Thus it is not recommended to use. It should only be used if you prefer speed over accuracy or you know that your target has sufficient overlap with the protocols and cipher provided by your openssl binary.
`--openssl ` testssl.sh tries first very hard to find the binary supplied (where the tree of testssl.sh resides, from the directory where testssl.sh has been started from, etc.). If all that doesn't work it falls back to openssl supplied from the OS (`$PATH`). With this option you can point testssl.sh to your binary of choice and override any internal magic to find the openssl binary. (Environment preset via `OPENSSL=`). Depending on your test parameters it could be faster to pick the OpenSSL version which has a bigger overlap in terms of ciphers protocols with the target. Also, when testing a modern server, OpenSSL 3.X is faster than older OpenSSL versions, or on MacOS 18, as opposed to the provided LibreSSL version.
@@ -179,7 +179,7 @@ Any single check switch supplied as an argument prevents testssl.sh from doing a
`-f, --fs, --nsa, --forward-secrecy` Checks robust forward secrecy key exchange. "Robust" means that ciphers having intrinsic severe weaknesses like Null Authentication or Encryption, 3DES and RC4 won't be considered here. There shouldn't be the wrong impression that a secure key exchange has been taking place and everything is fine when in reality the encryption sucks. Also this section lists the available elliptical curves and Diffie Hellman groups, as well as FFDHE groups (TLS 1.2 and TLS 1.3).
-`-p, --protocols` checks every SSL/TLS protocols: SSLv2, SSLv3, TLS 1.0 through TLS 1.3. And for HTTP also QUIC (HTTP/3), SPDY (NPN) and ALPN (HTTP/2). For TLS 1.3 the final version and several drafts (from 18 on) are tested. QUIC needs OpenSSL >= 3.2 which can be automatically picked up when in `/usr/bin/openssl` (or when defined environment variable OPENSSL2). If a TLS-1.3-only host is encountered and the openssl-bad version is used testssl.sh will e.g. for HTTP header checks switch to `/usr/bin/openssl` (or when defined via ENV to OPENSSL2). Also this will be tried for the QUIC check.
+`-p, --protocols` checks every SSL/TLS protocols: SSLv2, SSLv3, TLS 1.0 through TLS 1.3. And for HTTP also QUIC (HTTP/3), SPDY (NPN) and ALPN (HTTP/2). For TLS 1.3 the final version and several drafts (from 18 on) are tested. QUIC needs OpenSSL >= 3.2 which can be automatically picked up when in `/usr/bin/openssl` (or when defined environment variable OPENSSL2). If a TLS-1.3-only host is encountered and the openssl-bad version is used testssl.sh will e.g. for HTTP header checks switch to `/usr/bin/openssl` (or when defined via ENV to OPENSSL2). Also this will be tried for the QUIC check. You will get an additional message if the DNS HTTPS Resource Record matches the QUIC finding. Also if there are negative consequences (h3 advertised but not offered).
`-P, --server-preference, --preference` displays the servers preferences: cipher order, with used openssl client: negotiated protocol and cipher. If there's a cipher order enforced by the server it displays it for each protocol (openssl+sockets). If there's not, it displays instead which ciphers from the server were picked with each protocol.
@@ -532,6 +532,7 @@ Please note that for plain TLS-encrypted ports you must not specify the protocol
* RFC 8470: Using Early Data in HTTP
* RFC 8701: Applying Generate Random Extensions And Sustain Extensibility (GREASE) to TLS Extensibility
* RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport
+* RFC 9460: Service Binding and Parameter Specification via the DNS (SVCB and HTTPS Resource Records)
* W3C CSP: Content Security Policy Level 1-3
* TLSWG Draft: The Transport Layer Security (TLS) Protocol Version 1.3
* FIPS 203: Module-Lattice-Based Key-Encapsulation Mechanism Standard
From f284366aee57e844aefe5c533f63522e111df4de Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Mon, 22 Jun 2026 14:38:35 +0000
Subject: [PATCH 13/13] Auto-generate docs from testssl.1.md [skip ci]
---
doc/testssl.1 | 19 ++++++++++++++-----
doc/testssl.1.html | 22 ++++++++++++++--------
2 files changed, 28 insertions(+), 13 deletions(-)
diff --git a/doc/testssl.1 b/doc/testssl.1
index f0a0a58..1a7c5f2 100644
--- a/doc/testssl.1
+++ b/doc/testssl.1
@@ -50,9 +50,11 @@ of appearance):
.IP " 0)" 4
displays a banner (see below), does a DNS lookup also for further IP
addresses and does for the returned IP address a reverse lookup.
+Also the so called DNS HTTPS record is being queried and displayed (for
+the first IP only).
Last but not least a service check is being done.
.IP " 1)" 4
-SSL/TLS protocol check
+SSL/TLS protocol check plus QUIC and ALPN check
.IP " 2)" 4
standard cipher categories
.IP " 3)" 4
@@ -329,10 +331,11 @@ If you don\(cqt want this behavior, you need to supply \f[CR]\-4.\f[R]
of the target won\(cqt be scanned.
.PP
\f[CR]\-\-ssl\-native\f[R] Instead of using a mixture of bash sockets
-and a few openssl s_client connects, testssl.sh uses the latter (almost)
-only.
-This is faster but provides less accurate results, especially for the
-client simulation and for cipher support.
+and a few \f[CR]openssl s_client connect\f[R]s, testssl.sh uses the
+latter (almost) only.
+This is faster but doesn\(cqt provides accurate results, especially for
+the client simulation and for cipher support.
+Thus this is not recommended anymore.
For all checks you will see a warning if testssl.sh cannot tell if a
particular check cannot be performed.
For some checks however you might end up getting false negatives without
@@ -519,6 +522,9 @@ If a TLS\-1.3\-only host is encountered and the openssl\-bad version is
used testssl.sh will e.g.\ for HTTP header checks switch to
\f[CR]/usr/bin/openssl\f[R] (or when defined via ENV to OPENSSL2).
Also this will be tried for the QUIC check.
+You will get an additional message if the DNS HTTPS Resource Record
+matches the QUIC finding.
+Also if there are negative consequences (h3 advertised but not offered).
.PP
\f[CR]\-P, \-\-server\-preference, \-\-preference\f[R] displays the
servers preferences: cipher order, with used openssl client: negotiated
@@ -1422,6 +1428,9 @@ RFC 8701: Applying Generate Random Extensions And Sustain Extensibility
.IP \(bu 2
RFC 9000: QUIC: A UDP\-Based Multiplexed and Secure Transport
.IP \(bu 2
+RFC 9460: Service Binding and Parameter Specification via the DNS (SVCB
+and HTTPS Resource Records)
+.IP \(bu 2
W3C CSP: Content Security Policy Level 1\-3
.IP \(bu 2
TLSWG Draft: The Transport Layer Security (TLS) Protocol Version 1.3
diff --git a/doc/testssl.1.html b/doc/testssl.1.html
index 347a120..06a0010 100644
--- a/doc/testssl.1.html
+++ b/doc/testssl.1.html
@@ -84,9 +84,10 @@
displays a banner (see below), does a DNS lookup also for
further IP addresses and does for the returned IP address a
- reverse lookup. Last but not least a service check is being
- done.
- SSL/TLS protocol check
+ reverse lookup. Also the so called DNS HTTPS record is being
+ queried and displayed (for the first IP only). Last but not
+ least a service check is being done.
+ SSL/TLS protocol check plus QUIC and ALPN check
standard cipher categories
server’s cipher preferences (server order?)
forward secrecy: ciphers and elliptical curves
@@ -321,10 +322,11 @@
-4 scans only IPv4 addresses of the target, IPv6
addresses of the target won’t be scanned.
--ssl-native Instead of using a mixture of bash
- sockets and a few openssl s_client connects, testssl.sh uses the
- latter (almost) only. This is faster but provides less accurate
- results, especially for the client simulation and for cipher
- support. For all checks you will see a warning if testssl.sh
+ sockets and a few openssl s_client connects,
+ testssl.sh uses the latter (almost) only. This is faster but
+ doesn’t provides accurate results, especially for the client
+ simulation and for cipher support. Thus this is not recommended
+ anymore. For all checks you will see a warning if testssl.sh
cannot tell if a particular check cannot be performed. For some
checks however you might end up getting false negatives without
a warning. Thus it is not recommended to use. It should only be
@@ -483,7 +485,9 @@
the openssl-bad version is used testssl.sh will e.g. for HTTP
header checks switch to /usr/bin/openssl (or when
defined via ENV to OPENSSL2). Also this will be tried for the
- QUIC check.
+ QUIC check. You will get an additional message if the DNS HTTPS
+ Resource Record matches the QUIC finding. Also if there are
+ negative consequences (h3 advertised but not offered).
-P, --server-preference, --preference displays
the servers preferences: cipher order, with used openssl client:
negotiated protocol and cipher. If there’s a cipher order
@@ -1201,6 +1205,8 @@
Extensibility (GREASE) to TLS Extensibility
RFC 9000: QUIC: A UDP-Based Multiplexed and Secure
Transport
+ RFC 9460: Service Binding and Parameter Specification via
+ the DNS (SVCB and HTTPS Resource Records)
W3C CSP: Content Security Policy Level 1-3
TLSWG Draft: The Transport Layer Security (TLS) Protocol
Version 1.3