Skip to content

Commit e2404a8

Browse files
committed
Add MTA-STS as a PoC
This commit adds a first PoC implementation of MTA-STS (RFC 8461), see also issue #1646. What works: - test a hostname which is equal to a MX record and a domainname and has a MTS-STS setup (dev.testssl.sh) - check _mta-sts TXT record + https://mta-sts.$NODE/.well-known/mta-sts.txt - check also _smtp._tls TXT record - screen output What doesn't work - test a hostname which is not equal to domainname - test a hostname which has not mx record - fileout put - any parsing of TXT record + .well-known/mta-sts.txt - when no TXT records or .well-known/mta-sts.txt are there - fileoutput - colored screen output There's a stub function for DANE. There are also two stub functions splitting HTTP body from HTTP header which I couldn't get to work and will be removed later. Besides to avoid confusion it changes from all GET requests over HTTPS tm_out to safe_echo. It's actually exactly the same only the name is different.
1 parent 477bd13 commit e2404a8

File tree

1 file changed

+126
-4
lines changed

1 file changed

+126
-4
lines changed

testssl.sh

Lines changed: 126 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2210,7 +2210,7 @@ service_detection() {
22102210
# trying with sockets is better than not even trying.
22112211
tls_sockets "04" "$TLS13_CIPHER" "all+" "" "" false
22122212
if [[ $? -eq 0 ]]; then
2213-
plaintext="$(tm_out "$GET_REQ11" | hexdump -v -e '16/1 "%02X"')"
2213+
plaintext="$(safe_echo "$GET_REQ11" | hexdump -v -e '16/1 "%02X"')"
22142214
plaintext="${plaintext%%[!0-9A-F]*}"
22152215
send_app_data "$plaintext"
22162216
if [[ $? -eq 0 ]]; then
@@ -2225,7 +2225,7 @@ service_detection() {
22252225
fi
22262226
else
22272227
# SNI is not standardized for !HTTPS but fortunately for other protocols s_client doesn't seem to care
2228-
tm_out "$GET_REQ11" | $OPENSSL s_client $(s_client_options "$1 -quiet $BUGS -connect $NODEIP:$PORT $PROXY $SNI") >$TMPFILE 2>$ERRFILE &
2228+
safe_echo "$GET_REQ11" | $OPENSSL s_client $(s_client_options "$1 -quiet $BUGS -connect $NODEIP:$PORT $PROXY $SNI") >$TMPFILE 2>$ERRFILE &
22292229
wait_kill $! $HEADER_MAXSLEEP
22302230
was_killed=$?
22312231
fi
@@ -2321,12 +2321,12 @@ run_http_header() {
23212321

23222322
pr_bold " HTTP Status Code "
23232323
[[ -z "$1" ]] && url="/" || url="$1"
2324-
tm_out "$GET_REQ11" | $OPENSSL s_client $(s_client_options "$OPTIMAL_PROTO $BUGS -quiet -ign_eof -connect $NODEIP:$PORT $PROXY $SNI") >$HEADERFILE 2>$ERRFILE &
2324+
safe_echo "$GET_REQ11" | $OPENSSL s_client $(s_client_options "$OPTIMAL_PROTO $BUGS -quiet -ign_eof -connect $NODEIP:$PORT $PROXY $SNI") >$HEADERFILE 2>$ERRFILE &
23252325
wait_kill $! $HEADER_MAXSLEEP
23262326
if [[ $? -eq 0 ]]; then
23272327
# Issue HTTP GET again as it properly finished within $HEADER_MAXSLEEP and didn't hang.
23282328
# Doing it again in the foreground to get an accurate header time
2329-
tm_out "$GET_REQ11" | $OPENSSL s_client $(s_client_options "$OPTIMAL_PROTO $BUGS -quiet -ign_eof -connect $NODEIP:$PORT $PROXY $SNI") >$HEADERFILE 2>$ERRFILE
2329+
safe_echo "$GET_REQ11" | $OPENSSL s_client $(s_client_options "$OPTIMAL_PROTO $BUGS -quiet -ign_eof -connect $NODEIP:$PORT $PROXY $SNI") >$HEADERFILE 2>$ERRFILE
23302330
NOW_TIME=$(date "+%s")
23312331
HTTP_TIME=$(awk -F': ' '/^date:/ { print $2 } /^Date:/ { print $2 }' $HEADERFILE)
23322332
HAD_SLEPT=0
@@ -7354,6 +7354,124 @@ tls_time() {
73547354
return 0
73557355
}
73567356

7357+
# rfc8461
7358+
sub_mta_sts() {
7359+
local mta_sts_record=""
7360+
local policy=""
7361+
local smtp_tls_record=""
7362+
local spaces="$1"
7363+
local useragent="$UA_STD"
7364+
$SNEAKY && useragent="$UA_SNEAKY"
7365+
7366+
[[ ! "$STARTTLS_PROTOCOL" =~ smtp ]] && return 0
7367+
7368+
# This works currently only when the MX record is equal the domainname like with the testcase dev.testssl.sh
7369+
# So either we must only execute this when called --mx or we must deduce the domain name from $NODE somehow.
7370+
# For the latter we could reverse check again with get_mx_record whether the name passed later passed
7371+
# to this function is an mx record from this domain.
7372+
# So the plan is to chek whether $CMDLINE matches --mx. If not we check whether there is an MX record
7373+
# for $NODE which matches the current $NODE. If not we subsequently remove the leading hostname part of
7374+
# the $NODE and check whether this is a domainname and has a MX which matches the original node.
7375+
# If we end up @ DOMAIN.TLD and didn't find anything we emit a message and return.
7376+
7377+
pr_bold " MTA-STS Policy "
7378+
7379+
mta_sts_record="$(get_txt_record _mta-sts.$NODE)"
7380+
# look for exact match for 'v=STSv1'
7381+
# look for exact match for 'id='
7382+
7383+
# echo "$mta_sts_record"; echo
7384+
7385+
policy="$(safe_echo "GET /.well-known/mta-sts.txt HTTP/1.1\r\nHost: mta-sts.$NODE\r\nUser-Agent: $useragent\r\nAccept-Encoding: identity\r\nAccept: text/*\r\nConnection: Close\r\n\r\n" | $OPENSSL s_client $(s_client_options "-quiet -ign_eof -connect $NODEIP:443 $PROXY $SNI") 2>$ERRFILE)"
7386+
# here also the openssl return val needs to be checked
7387+
7388+
#tmp="$(printf "$policy" | awk '/^$/ { p=1;next } { if(!p) { print } }')"
7389+
# policy="$(awk '/^$/ { p=1;next } { if(!p) { print } }' <<< "$policy")"
7390+
policy="$(print_after_blankline "$policy")"
7391+
#echo "POLICY2: $tmp "
7392+
# echo "$policy"; echo
7393+
7394+
# header needs to be stripped. Either the lower bytes which come after Content-Length in the header.
7395+
# or starting from version or starting after blank line
7396+
7397+
# check policy:
7398+
# - grep -Ew 'version|mode|mx|max_age'
7399+
# - version.*STSv1$
7400+
# - grep 'mode:.*testing|mode:.*enforce'
7401+
# - grep 'max_age:.*[0-9](5-10)'
7402+
# - max_age should be sufficient otherwise caching it is ~useless, see HSTS
7403+
# - whether mx record matches
7404+
7405+
if [[ $DEBUG -ge 1 ]]; then
7406+
echo "$mta_sts_record" >$TMPFILE/_mta-sts.$NODE.txt
7407+
echo "$policy" >$TMPFILE/$NODE.mta-sts.well-known_mta-sts.txt
7408+
echo "$smtp_tls_record" > $TMPFILE/_smtp._tls.$NODE
7409+
fi
7410+
7411+
smtp_tls_record="$(get_txt_record _smtp._tls.$NODE)"
7412+
7413+
outln "valid _mta-sts TXT record \"$mta_sts_record\""
7414+
out "$spaces"
7415+
outln "valid enforced policy \"https://mta-sts.$NODE/.well-known/mta-sts.txt\""
7416+
out "$spaces"
7417+
outln "optional _smtp._tls TXT record \"$smtp_tls_record\""
7418+
7419+
return 0
7420+
}
7421+
7422+
# e.g. for removing the HTTP header
7423+
#
7424+
print_after_blankline() {
7425+
# doesn't work (oneliner with $1 instead of multiline):
7426+
#awk '/^$/ { p=1;next } { if(p) { print } }' <<< $1
7427+
local first=true
7428+
local line=""
7429+
7430+
while read -r line; do
7431+
if ! "$first"; then
7432+
safe_echo "$line\n"
7433+
else
7434+
# ignore everything until we hit an empty line or a line with a blank or a CR / LF
7435+
if [[ -z "$line" ]] || [[ "$line" =~ ^[[:space:]]$ ]]; then
7436+
first=false
7437+
continue
7438+
fi
7439+
fi
7440+
done <<< $1
7441+
set +x
7442+
}
7443+
7444+
# e.g. for removing the body
7445+
#
7446+
print_before_blankline() {
7447+
# doesn't work (oneliner with $1 instead of multiline):
7448+
awk '/^$/ { p=1;next } { if(!p) { print } }' <<< $1
7449+
}
7450+
7451+
7452+
# RFC 6394
7453+
# RFC 6698
7454+
# RFC 7218
7455+
# RFC 7671
7456+
# RFC 7672
7457+
# RFC 7673
7458+
sub_dane() {
7459+
local tlsa_record=""
7460+
local rrsig_record=""
7461+
local spaces="$1"
7462+
7463+
# Not yet implemeted
7464+
return 0
7465+
7466+
pr_bold " DANE / DNSSEC "
7467+
7468+
tlsa_record="$(get_tlsa_record _$PORT._tcp.$NODE)"
7469+
# parsing TLSA certificate usage, TLSA selector, TLSA matching type, hash
7470+
rrsig_record="$(get_rrsig_record $NODE)"
7471+
7472+
# return 0
7473+
}
7474+
73577475
# core function determining whether handshake succeeded or not
73587476
# arg1: return value of "openssl s_client connect"
73597477
# arg2: temporary file with the server hello
@@ -9475,6 +9593,7 @@ run_server_defaults() {
94759593
local -a -i success
94769594
local cn_nosni cn_sni sans_nosni sans_sni san tls_extensions
94779595
local using_sockets=true
9596+
local spaces=" "
94789597

94799598
"$SSL_NATIVE" && using_sockets=false
94809599

@@ -9821,6 +9940,9 @@ run_server_defaults() {
98219940

98229941
tls_time
98239942

9943+
sub_mta_sts "$spaces"
9944+
sub_dane "$spaces"
9945+
98249946
if [[ -n "$SNI" ]] && [[ $certs_found -ne 0 ]] && [[ ! -e $HOSTCERT.nosni ]]; then
98259947
# no cipher suites specified here. We just want the default vhost subject
98269948
if ! "$HAS_TLS13" && [[ $(has_server_protocol "tls1_3") -eq 0 ]]; then

0 commit comments

Comments
 (0)