Skip to content

Commit ec4ceb2

Browse files
authored
Add mTLS feature
Added new feature to support mutual TLS via client certificate and private key, when a remote server requires client authentication.
1 parent ad04a90 commit ec4ceb2

1 file changed

Lines changed: 63 additions & 18 deletions

File tree

testssl.sh

Lines changed: 63 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,7 @@ XMPP_HOST=""
389389
PROXYIP="" # $PROXYIP:$PROXPORT is your proxy if --proxy is defined ...
390390
PROXYPORT="" # ... and openssl has proxy support
391391
PROXY="" # Once check_proxy() executed it contains $PROXYIP:$PROXPORT
392+
MTLS="" # mTLS authentication with client certificate and private key
392393
VULN_COUNT=0
393394
SERVICE="" # Is the server running an HTTP server, SMTP, POP or IMAP?
394395
URI=""
@@ -2316,6 +2317,12 @@ s_client_options() {
23162317
fi
23172318
# $keyopts may be set as an environment variable to enable client authentication (see PR #1383)
23182319
tm_out "$options $keyopts"
2320+
2321+
# In case of mutual TLS authentication is required by the server
2322+
# Note: the PEM certificate file must contain: client certificate and certificate key (not encrypted)
2323+
if [[ -n "$MTLS" ]]; then
2324+
options+=" -cert $MTLS"
2325+
fi
23192326
}
23202327

23212328
###### check code starts here ######
@@ -2375,10 +2382,14 @@ service_detection() {
23752382
out " $SERVICE, thus skipping HTTP specific checks"
23762383
fileout "${jsonID}" "INFO" "$SERVICE, thus skipping HTTP specific checks"
23772384
;;
2378-
*) if [[ "$CLIENT_AUTH" == required ]]; then
2379-
out " certificate-based authentication => skipping all HTTP checks"
2380-
echo "certificate-based authentication => skipping all HTTP checks" >$TMPFILE
2381-
fileout "${jsonID}" "INFO" "certificate-based authentication => skipping all HTTP checks"
2385+
*) if [[ ! -z $MTLS ]]; then
2386+
out " not identified, but mTLS authentication is set ==> trying HTTP checks"
2387+
SERVICE=HTTP
2388+
fileout "${jsonID}" "DEBUG" "Couldn't determine service -- ASSUME_HTTP set"
2389+
elif [[ "$CLIENT_AUTH" == required ]] && [[ -z $MTLS ]]; then
2390+
out " certificate-based authentication without providing client certificate and private key => skipping all HTTP checks"
2391+
echo "certificate-based authentication without providing client certificate and private key => skipping all HTTP checks" >$TMPFILE
2392+
fileout "${jsonID}" "INFO" "certificate-based authentication without providing client certificate and private key => skipping all HTTP checks"
23822393
else
23832394
out " Couldn't determine what's running on port $PORT"
23842395
if "$ASSUME_HTTP"; then
@@ -2430,6 +2441,7 @@ run_http_header() {
24302441
local url redirect
24312442
local jsonID="HTTP_status_code"
24322443
local spaces=" "
2444+
local cert_option=""
24332445

24342446
HEADERFILE=$TEMPDIR/$NODEIP.http_header.txt
24352447
if [[ $NR_HEADER_FAIL -eq 0 ]]; then
@@ -2444,12 +2456,17 @@ run_http_header() {
24442456

24452457
pr_bold " HTTP Status Code "
24462458
[[ -z "$1" ]] && url="/" || url="$1"
2447-
tm_out "$GET_REQ11" | $OPENSSL s_client $(s_client_options "$OPTIMAL_PROTO $BUGS -quiet -ign_eof -connect $NODEIP:$PORT $PROXY $SNI") >$HEADERFILE 2>$ERRFILE &
2459+
2460+
# Set -cert option value if mTLS authentication is selected
2461+
if [[ ! -z "$MTLS" ]]; then
2462+
cert_option="-cert $MTLS"
2463+
fi
2464+
tm_out "$GET_REQ11" | $OPENSSL s_client $(s_client_options "$OPTIMAL_PROTO $BUGS $cert_option -quiet -ign_eof -connect $NODEIP:$PORT $PROXY $SNI") >$HEADERFILE 2>$ERRFILE &
24482465
wait_kill $! $HEADER_MAXSLEEP
24492466
if [[ $? -eq 0 ]]; then
24502467
# Issue HTTP GET again as it properly finished within $HEADER_MAXSLEEP and didn't hang.
24512468
# Doing it again in the foreground to get an accurate header time
2452-
tm_out "$GET_REQ11" | $OPENSSL s_client $(s_client_options "$OPTIMAL_PROTO $BUGS -quiet -ign_eof -connect $NODEIP:$PORT $PROXY $SNI") >$HEADERFILE 2>$ERRFILE
2469+
tm_out "$GET_REQ11" | $OPENSSL s_client $(s_client_options "$OPTIMAL_PROTO $BUGS $cert_option -quiet -ign_eof -connect $NODEIP:$PORT $PROXY $SNI") >$HEADERFILE 2>$ERRFILE
24532470
NOW_TIME=$(date "+%s")
24542471
HTTP_TIME=$(awk -F': ' '/^date:/ { print $2 } /^Date:/ { print $2 }' $HEADERFILE)
24552472
HTTP_AGE=$(awk -F': ' '/^[aA][gG][eE]: / { print $2 }' $HEADERFILE)
@@ -2601,7 +2618,7 @@ run_http_date() {
26012618
local spaces=" "
26022619
jsonID="HTTP_clock_skew"
26032620

2604-
if [[ $SERVICE != HTTP ]] || [[ "$CLIENT_AUTH" == required ]]; then
2621+
if [[ $SERVICE != HTTP ]] || { [[ "$CLIENT_AUTH" == required ]] && [[ -z "$MTLS" ]]; }; then
26052622
return 0
26062623
fi
26072624
if [[ ! -s $HEADERFILE ]]; then
@@ -6710,6 +6727,12 @@ sub_session_resumption() {
67106727
local sess_data=$(mktemp $TEMPDIR/sub_session_data_resumption.$NODEIP.XXXXXX)
67116728
local -a rw_line
67126729
local protocol="$1"
6730+
local cert_option=""
6731+
6732+
# Set -cert option value if mTLS authentication is selected
6733+
if [[ ! -z "$MTLS" ]]; then
6734+
cert_option="-cert $MTLS"
6735+
fi
67136736

67146737
if [[ "$2" == ID ]]; then
67156738
local byID=true
@@ -6721,7 +6744,8 @@ sub_session_resumption() {
67216744
return 1
67226745
fi
67236746
fi
6724-
[[ "$CLIENT_AUTH" == required ]] && return 6
6747+
# Return 6 if client authentication is required and none PEM file (containing client certificate+private key) is provided
6748+
[[ "$CLIENT_AUTH" == required ]] && [[ -z "$MTLS" ]] && return 6
67256749
if ! "$HAS_TLS13" && "$HAS_NO_SSL2"; then
67266750
addcmd+=" -no_ssl2"
67276751
else
@@ -6738,7 +6762,7 @@ sub_session_resumption() {
67386762
addcmd+=" $protocol"
67396763
fi
67406764

6741-
$OPENSSL s_client $(s_client_options "$STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY $SNI $addcmd -sess_out $sess_data") </dev/null &>$tmpfile
6765+
$OPENSSL s_client $(s_client_options "$STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY $SNI $cert_option $addcmd -sess_out $sess_data") </dev/null &>$tmpfile
67426766
ret1=$?
67436767
if [[ $ret1 -ne 0 ]]; then
67446768
# MacOS and LibreSSL return 1 here, that's why we need to check whether the handshake contains e.g. a certificate
@@ -6756,7 +6780,7 @@ sub_session_resumption() {
67566780
# [[ ! $(<$sess_data) =~ -----.*\ SSL\ SESSION\ PARAMETERS----- ]]
67576781
ret=2
67586782
else
6759-
$OPENSSL s_client $(s_client_options "$STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY $SNI $addcmd -sess_in $sess_data") </dev/null >$tmpfile 2>$ERRFILE
6783+
$OPENSSL s_client $(s_client_options "$STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY $SNI $cert_option $addcmd -sess_in $sess_data") </dev/null >$tmpfile 2>$ERRFILE
67606784
ret2=$?
67616785
if [[ $DEBUG -ge 2 ]]; then
67626786
echo -n "$ret1, $ret2, "
@@ -17037,9 +17061,9 @@ run_renego() {
1703717061
[[ $DEBUG -ge 1 ]] && out ", no renegotiation support in TLS 1.3 only servers"
1703817062
outln
1703917063
fileout "$jsonID" "OK" "not vulnerable, TLS 1.3 only" "$cve" "$cwe"
17040-
elif [[ "$CLIENT_AUTH" == required ]]; then
17041-
prln_warning "client x509-based authentication prevents this from being tested"
17042-
fileout "$jsonID" "WARN" "client x509-based authentication prevents this from being tested"
17064+
elif [[ "$CLIENT_AUTH" == required ]] && [[ -z "$MTLS" ]]; then
17065+
prln_warning "not having provided client certificate and private key file, the client x509-based authentication prevents this from being tested"
17066+
fileout "$jsonID" "WARN" "not having provided client certificate and private key file, the client x509-based authentication prevents this from being tested"
1704317067
sec_client_renego=1
1704417068
else
1704517069
# We will need $ERRFILE for mitigation detection
@@ -17203,7 +17227,7 @@ run_crime() {
1720317227
fileout "$jsonID" "OK" "not vulnerable" "$cve" "$cwe"
1720417228
fi
1720517229
else
17206-
if [[ $SERVICE == HTTP ]] || [[ "$CLIENT_AUTH" == required ]]; then
17230+
if [[ $SERVICE == HTTP ]] || [[ "$CLIENT_AUTH" == required ]] || [[ ! -z "$MTLS" ]]; then
1720717231
pr_svrty_high "VULNERABLE (NOT ok)"
1720817232
fileout "$jsonID" "HIGH" "VULNERABLE" "$cve" "$cwe" "$hint"
1720917233
else
@@ -17262,8 +17286,13 @@ sub_breach_helper() {
1726217286
local get_command="$1"
1726317287
local detected_compression=""
1726417288
local -i was_killed=0
17289+
local cert_option=""
1726517290

17266-
safe_echo "$get_command" | $OPENSSL s_client $(s_client_options "$OPTIMAL_PROTO $BUGS -quiet -ign_eof -connect $NODEIP:$PORT $PROXY $SNI") 1>$TMPFILE 2>$ERRFILE &
17291+
# Set -cert option value if mTLS authentication is selected
17292+
if [[ ! -z "$MTLS" ]]; then
17293+
cert_option="-cert $MTLS"
17294+
fi
17295+
safe_echo "$get_command" | $OPENSSL s_client $(s_client_options "$OPTIMAL_PROTO $BUGS $cert_option -quiet -ign_eof -connect $NODEIP:$PORT $PROXY $SNI") 1>$TMPFILE 2>$ERRFILE &
1726717296
wait_kill $! $HEADER_MAXSLEEP
1726817297
was_killed=$? # !=0 when it was killed
1726917298
detected_compression=$(grep -ia ^Content-Encoding: $TMPFILE)
@@ -17313,9 +17342,9 @@ run_breach() {
1731317342

1731417343
[[ $VULN_COUNT -le $VULN_THRESHLD ]] && outln && pr_headlineln " Testing for BREACH (HTTP compression) vulnerability " && outln
1731517344
pr_bold " BREACH"; out " ($cve) "
17316-
if [[ "$CLIENT_AUTH" == required ]]; then
17317-
prln_warning "client x509-based authentication prevents this from being tested"
17318-
fileout "$jsonID" "WARN" "client x509-based authentication prevents this from being tested" "$cve" "$cwe"
17345+
if [[ "$CLIENT_AUTH" == required ]] && [[ -z "$MTLS" ]]; then
17346+
prln_warning "not having provided client certificate and private key file, the client x509-based authentication prevents this from being tested"
17347+
fileout "$jsonID" "WARN" "not having provided client certificate and private key file, the client x509-based authentication prevents this from being tested" "$cve" "$cwe"
1731917348
return 7
1732017349
fi
1732117350

@@ -20500,6 +20529,7 @@ tuning / connect options (most also can be preset via environment variables):
2050020529
--ids-friendly skips a few vulnerability checks which may cause IDSs to block the scanning IP
2050120530
--phone-out allow to contact external servers for CRL download and querying OCSP responder
2050220531
--add-ca <CA files|CA dir> path to <CAdir> with *.pem or a comma separated list of CA files to include in trust check
20532+
--mtls <CLIENT CERT file> path to <CLIENT CERT> file, it must be in PEM format and contain client certificate with certificate key (not encrypted)
2050320533
--basicauth <user:pass> provide HTTP basic auth information.
2050420534
--reqheader <header> add custom http request headers
2050520535

@@ -23807,6 +23837,10 @@ parse_cmd_line() {
2380723837
OPENSSL_TIMEOUT="$(parse_opt_equal_sign "$1" "$2")"
2380823838
[[ $? -eq 0 ]] && shift
2380923839
;;
23840+
--mtls|--mtls=*)
23841+
MTLS="$(parse_opt_equal_sign "$1" "$2")"
23842+
[[ $? -eq 0 ]] && shift
23843+
;;
2381023844
--connect-timeout|--connect-timeout=*)
2381123845
CONNECT_TIMEOUT="$(parse_opt_equal_sign "$1" "$2")"
2381223846
[[ $? -eq 0 ]] && shift
@@ -23885,6 +23919,17 @@ parse_cmd_line() {
2388523919
grep -q 'BEGIN CERTIFICATE' "$fname" || fatal_cmd_line "\"$fname\" is not CA file in PEM format" $ERR_RESOURCE
2388623920
done
2388723921

23922+
# Check if mTLS has been selected, and if the correct client auth PEM file has been provided by user
23923+
if [[ ! -z "$MTLS" ]]; then
23924+
if [[ -f $MTLS ]]; then
23925+
grep -q 'BEGIN CERTIFICATE' "$MTLS" || fatal_cmd_line "\"$MTLS\" is not a client certificate file in PEM format" $ERR_RESOURCE
23926+
grep -q 'BEGIN PRIVATE KEY\|BEGIN RSA PRIVATE KEY' "$MTLS" || fatal_cmd_line "\"$MTLS\" the not encrypted private key is missing in the specified PEM file" $ERR_RESOURCE
23927+
MTLS=$MTLS
23928+
else
23929+
[[ -s "$MTLS" ]] || fatal_cmd_line "the specified client certificate file \"$MTLS\" does not exist" $ERR_RESOURCE
23930+
fi
23931+
fi
23932+
2388823933
"$FAST" && pr_warning "\n'--fast' can have some undesired side effects thus it is not recommended to use anymore\n"
2388923934
"$SSL_NATIVE" && pr_warning "\nusage of '--ssl-native' is not recommended as it will return incomplete and may even return incorrect results\n"
2389023935

0 commit comments

Comments
 (0)