From b5595a92058146e789bf7ac21aba03a31a6c9d33 Mon Sep 17 00:00:00 2001 From: David Cooper Date: Thu, 28 Jun 2018 14:15:55 -0400 Subject: [PATCH 1/2] Check stapled OCSP response for revocation status In cases in which the server offers a stapled OCSP response, this commit extracts the OCSP response and then checks the response for the status of the server's certificate. The check is performed in the same way as when the certificate includes an OCSP URI and the "--phone-out" option is set, except that the OCSP response is received from the TLS server rather than coming directly from the OCSP responder. Since this only involves additional processing of data that testssl.sh is already receiving, the check is performed whether or not the "--phone-out" flag is set. --- testssl.sh | 119 +++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 93 insertions(+), 26 deletions(-) diff --git a/testssl.sh b/testssl.sh index 09a0040..816f616 100755 --- a/testssl.sh +++ b/testssl.sh @@ -249,6 +249,7 @@ NO_ENGINE=${NO_ENGINE:-false} # if there are problems finding the (ext declare -r CLIENT_MIN_PFS=5 # number of ciphers needed to run a test for PFS CAPATH="${CAPATH:-/etc/ssl/certs/}" # Does nothing yet (FC has only a CA bundle per default, ==> openssl version -d) GOOD_CA_BUNDLE="" # A bundle of CA certificates that can be used to validate the server's certificate +STAPLED_OCSP_RESPONSE="" MEASURE_TIME_FILE=${MEASURE_TIME_FILE:-""} if [[ -n "$MEASURE_TIME_FILE" ]] && [[ -z "$MEASURE_TIME" ]]; then MEASURE_TIME=true @@ -1516,26 +1517,35 @@ check_revocation_crl() { check_revocation_ocsp() { local uri="$1" - local jsonID="$2" + local stapled_response="$2" + local jsonID="$3" local tmpfile="" local -i success local response="" local host_header="" - "$PHONE_OUT" || return 0 + "$PHONE_OUT" || [[ -n "$stapled_response" ]] || return 0 [[ -n "$GOOD_CA_BUNDLE" ]] || return 0 grep -q "\-\-\-\-\-BEGIN CERTIFICATE\-\-\-\-\-" $TEMPDIR/intermediatecerts.pem || return 0 tmpfile=$TEMPDIR/${NODE}-${NODEIP}.${uri##*\/} || exit $ERR_FCREATE - host_header=${uri##http://} - host_header=${host_header%%/*} - if [[ $OSSL_VER_MAJOR.$OSSL_VER_MINOR == "1.1.0"* ]] || [[ $OSSL_VER_MAJOR.$OSSL_VER_MINOR == "1.1.1"* ]]; then - host_header="-header Host=${host_header}" + if [[ -n "$stapled_response" ]]; then + > "$TEMPDIR/stabled_ocsp_response.dd" + asciihex_to_binary_file "$stapled_response" "$TEMPDIR/stabled_ocsp_response.dd" + $OPENSSL ocsp -no_nonce -respin "$TEMPDIR/stabled_ocsp_response.dd" \ + -issuer $TEMPDIR/hostcert_issuer.pem -verify_other $TEMPDIR/intermediatecerts.pem \ + -CAfile <(cat $ADDITIONAL_CA_FILES "$GOOD_CA_BUNDLE") -cert $HOSTCERT -text &> "$tmpfile" else - host_header="-header Host ${host_header}" + host_header=${uri##http://} + host_header=${host_header%%/*} + if [[ $OSSL_VER_MAJOR.$OSSL_VER_MINOR == "1.1.0"* ]] || [[ $OSSL_VER_MAJOR.$OSSL_VER_MINOR == "1.1.1"* ]]; then + host_header="-header Host=${host_header}" + else + host_header="-header Host ${host_header}" + fi + $OPENSSL ocsp -no_nonce ${host_header} -url "$uri" \ + -issuer $TEMPDIR/hostcert_issuer.pem -verify_other $TEMPDIR/intermediatecerts.pem \ + -CAfile <(cat $ADDITIONAL_CA_FILES "$GOOD_CA_BUNDLE") -cert $HOSTCERT -text &> "$tmpfile" fi - $OPENSSL ocsp -no_nonce ${host_header} -url "$uri" \ - -issuer $TEMPDIR/hostcert_issuer.pem -verify_other $TEMPDIR/intermediatecerts.pem \ - -CAfile <(cat $ADDITIONAL_CA_FILES "$GOOD_CA_BUNDLE") -cert $HOSTCERT -text &> "$tmpfile" if [[ $? -eq 0 ]] && grep -Fq "Response verify OK" "$tmpfile"; then response="$(grep -F "$HOSTCERT: " "$tmpfile")" response="${response#$HOSTCERT: }" @@ -6580,6 +6590,53 @@ extract_certificates() { return $success } +extract_stapled_ocsp() { + local response="$(cat $TMPFILE)" + local ocsp tmp + local -i ocsp_len + + STAPLED_OCSP_RESPONSE="" + if [[ "$response" =~ "CertificateStatus" ]]; then + # This is OpenSSL 1.1.0 or 1.1.1 and the response + # is TLS 1.2 or earlier. + ocsp="${response##*CertificateStatus}" + ocsp="16${ocsp#*16}" + ocsp="${ocsp%%<<<*}" + ocsp="$(strip_spaces "$(newline_to_spaces "$ocsp")")" + ocsp="${ocsp:8}" + elif [[ "$response" =~ "TLS server extension \"status request\" (id=5), len=0" ]]; then + # This is not OpenSSL 1.1.0 or 1.1.1, and the response + # is TLS 1.2 or earlier. + ocsp="${response%%OCSP response:*}" + ocsp="${ocsp##*<<<}" + ocsp="16${ocsp#*16}" + ocsp="$(strip_spaces "$(newline_to_spaces "$ocsp")")" + ocsp="${ocsp:8}" + elif [[ "$response" =~ "TLS server extension \"status request\" (id=5), len=" ]]; then + # This is OpenSSL 1.1.1 and the response is TLS 1.3. + ocsp="${response##*TLS server extension \"status request\" (id=5), len=}" + ocsp="${ocsp%%<<<*}" + tmp="${ocsp%%[!0-9]*}" + ocsp="${ocsp#$tmp}" + ocsp_len=2*$tmp + ocsp="$(awk ' { print $3 $4 $5 $6 $7 $8 $9 $10 $11 $12 $13 $14 $15 $16 $17 } ' <<< "$ocsp" | sed 's/-//')" + ocsp="$(strip_spaces "$(newline_to_spaces "$ocsp")")" + ocsp="${ocsp:0:ocsp_len}" + else + return 0 + fi + # Determine whether this is a single OCSP response or a sequence of + # responses and then extract just the response for the server's + # certificate. + if [[ "${ocsp:0:2}" == "01" ]]; then + STAPLED_OCSP_RESPONSE="${ocsp:8}" + elif [[ "${ocsp:0:2}" == "02" ]]; then + ocsp_len=2*$(hex2dec "${tls_certificate_status_ascii:8:6}") + STAPLED_OCSP_RESPONSE="${ocsp:14:ocsp_len}" + fi + return 0 +} + # arg1 is "-cipher " or empty # arg2 is a list of protocols to try (tls1_2, tls1_1, tls1, ssl3) or empty (if all should be tried) get_server_certificate() { @@ -6591,15 +6648,16 @@ get_server_certificate() { [[ $(has_server_protocol "tls1_3") -eq 1 ]] && return 1 if "$HAS_TLS13"; then if [[ "$1" =~ "-cipher tls1_3_RSA" ]]; then - $OPENSSL s_client $(s_client_options "$STARTTLS $BUGS -showcerts -connect $NODEIP:$PORT $PROXY $SNI -tls1_3 -tlsextdebug -status -sigalgs PSS+SHA256:PSS+SHA384") $ERRFILE >$TMPFILE + $OPENSSL s_client $(s_client_options "$STARTTLS $BUGS -showcerts -connect $NODEIP:$PORT $PROXY $SNI -tls1_3 -tlsextdebug -status -msg -sigalgs PSS+SHA256:PSS+SHA384") $ERRFILE >$TMPFILE elif [[ "$1" =~ "-cipher tls1_3_ECDSA" ]]; then - $OPENSSL s_client $(s_client_options "$STARTTLS $BUGS -showcerts -connect $NODEIP:$PORT $PROXY $SNI -tls1_3 -tlsextdebug -status -sigalgs ECDSA+SHA256:ECDSA+SHA384") $ERRFILE >$TMPFILE + $OPENSSL s_client $(s_client_options "$STARTTLS $BUGS -showcerts -connect $NODEIP:$PORT $PROXY $SNI -tls1_3 -tlsextdebug -status -msg -sigalgs ECDSA+SHA256:ECDSA+SHA384") $ERRFILE >$TMPFILE else return 1 fi sclient_connect_successful $? $TMPFILE || return 1 DETECTED_TLS_VERSION="0304" extract_certificates "tls1_3" + extract_stapled_ocsp success=$? else if [[ "$1" =~ "-cipher tls1_3_RSA" ]]; then @@ -6650,7 +6708,7 @@ get_server_certificate() { [[ 1 -eq $(has_server_protocol $proto) ]] && continue [[ "$proto" == "ssl3" ]] && ! "$HAS_SSL3" && continue addcmd="" - $OPENSSL s_client $(s_client_options "$STARTTLS $BUGS $1 -showcerts -connect $NODEIP:$PORT $PROXY $SNI -$proto -tlsextdebug $npn_params -status") $ERRFILE >$TMPFILE + $OPENSSL s_client $(s_client_options "$STARTTLS $BUGS $1 -showcerts -connect $NODEIP:$PORT $PROXY $SNI -$proto -tlsextdebug $npn_params -status -msg") $ERRFILE >$TMPFILE if sclient_connect_successful $? $TMPFILE; then success=0 grep -a 'TLS server extension' $TMPFILE >>$TEMPDIR/tlsext.txt @@ -6680,6 +6738,7 @@ get_server_certificate() { esac extract_new_tls_extensions $TMPFILE extract_certificates "$proto" + extract_stapled_ocsp success=$? tmpfile_handle ${FUNCNAME[0]}.txt @@ -6960,10 +7019,11 @@ certificate_info() { local cipher=$4 local cert_keysize=$5 local cert_type="$6" - local ocsp_response=$7 - local ocsp_response_status=$8 - local sni_used=$9 - local ct="${10}" + local ocsp_response_binary="$7" + local ocsp_response=$8 + local ocsp_response_status=$9 + local sni_used="${10}" + local ct="${11}" local cert_sig_algo cert_sig_hash_algo cert_key_algo cert_keyusage cert_ext_keyusage local outok=true local expire days2expire secs2warn ocsp_uri crl @@ -7636,7 +7696,7 @@ certificate_info() { if [[ $(count_lines "$ocsp_uri") -eq 1 ]]; then out "$ocsp_uri" if [[ "$expfinding" != "expired" ]]; then - check_revocation_ocsp "$ocsp_uri" "cert_ocspRevoked${json_postfix}" + check_revocation_ocsp "$ocsp_uri" "" "cert_ocspRevoked${json_postfix}" fi ret=$((ret +$?)) outln @@ -7650,7 +7710,7 @@ certificate_info() { fi out "$line" if [[ "$expfinding" != "expired" ]]; then - check_revocation_ocsp "$line" "cert_ocspRevoked${json_postfix}" + check_revocation_ocsp "$line" "" "cert_ocspRevoked${json_postfix}" ret=$((ret +$?)) fi outln @@ -7680,6 +7740,7 @@ certificate_info() { pr_svrty_good "offered" fileout "${jsonID}${json_postfix}" "OK" "offered" provides_stapling=true + check_revocation_ocsp "" "$ocsp_response_binary" "cert_ocspRevoked${json_postfix}" else if $GOST_STATUS_PROBLEM; then pr_warning "(GOST servers make problems here, sorry)" @@ -7752,7 +7813,7 @@ run_server_defaults() { local -i certs_found=0 local -i ret=0 local -a previous_hostcert previous_hostcert_txt previous_hostcert_type previous_hostcert_issuer previous_intermediates keysize cipher - local -a ocsp_response ocsp_response_status sni_used tls_version ct + local -a ocsp_response_binary ocsp_response ocsp_response_status sni_used tls_version ct local -a ciphers_to_test certificate_type local -a -i success local cn_nosni cn_sni sans_nosni sans_sni san tls_extensions @@ -7873,6 +7934,7 @@ run_server_defaults() { # If an OCSP response was sent, then get the full # response so that certificate_info() can determine # whether it includes a certificate transparency extension. + ocsp_response_binary[certs_found]="$STAPLED_OCSP_RESPONSE" if grep -a "OCSP response:" $TMPFILE | grep -q "no response sent"; then ocsp_response[certs_found]="$(grep -a "OCSP response" $TMPFILE)" else @@ -8056,7 +8118,8 @@ run_server_defaults() { echo "${previous_hostcert_issuer[i]}" > $TEMPDIR/hostcert_issuer.pem certificate_info "$i" "$certs_found" "${previous_hostcert_txt[i]}" \ "${cipher[i]}" "${keysize[i]}" "${previous_hostcert_type[i]}" \ - "${ocsp_response[i]}" "${ocsp_response_status[i]}" "${sni_used[i]}" "${ct[i]}" + "${ocsp_response_binary[i]}" "${ocsp_response[i]}" \ + "${ocsp_response_status[i]}" "${sni_used[i]}" "${ct[i]}" [[ $? -ne 0 ]] && ((ret++)) done return $ret @@ -11034,8 +11097,11 @@ parse_tls_serverhello() { echo " i:${CAissuerDN:8}" >> $TMPFILE echo "$pem_certificate" >> $TMPFILE echo "$pem_certificate" >> "$TEMPDIR/intermediatecerts.pem" - if [[ -z "$hostcert_issuer" ]] && [[ $tls_certificate_status_ascii_len -ne 0 ]]; then - hostcert_issuer=$(mktemp $TEMPDIR/pem_cert.XXXXXX) || return 1 + if [[ -z "$hostcert_issuer" ]]; then + # The issuer's certificate is needed if there is a stapled OCSP response, + # and it may be needed if check_revocation_ocsp() will later be called + # with the OCSP URI in the server's certificate. + hostcert_issuer="$TEMPDIR/hostcert_issuer.pem" echo "$pem_certificate" > "$hostcert_issuer" fi done @@ -11077,15 +11143,16 @@ parse_tls_serverhello() { fi ocsp_resp_offset=14 fi + STAPLED_OCSP_RESPONSE="" if [[ $ocsp_response_len -ne 0 ]]; then + STAPLED_OCSP_RESPONSE="${tls_certificate_status_ascii:ocsp_resp_offset:ocsp_response_len}" echo "OCSP response:" >> $TMPFILE echo "===============================================================================" >> $TMPFILE if [[ -n "$hostcert_issuer" ]]; then - asciihex_to_binary_file "${tls_certificate_status_ascii:ocsp_resp_offset:ocsp_response_len}" "/dev/stdout" | \ + asciihex_to_binary_file "$STAPLED_OCSP_RESPONSE" "/dev/stdout" | \ $OPENSSL ocsp -no_nonce -CAfile $TEMPDIR/intermediatecerts.pem -issuer $hostcert_issuer -cert $HOSTCERT -respin /dev/stdin -resp_text >> $TMPFILE 2>$ERRFILE - rm "$hostcert_issuer" else - asciihex_to_binary_file "${tls_certificate_status_ascii:ocsp_resp_offset:ocsp_response_len}" "/dev/stdout" | \ + asciihex_to_binary_file "$STAPLED_OCSP_RESPONSE" "/dev/stdout" | \ $OPENSSL ocsp -respin /dev/stdin -resp_text >> $TMPFILE 2>$ERRFILE fi echo "===============================================================================" >> $TMPFILE From 72ef69aeaee2d79608427714d214b2202e50d459 Mon Sep 17 00:00:00 2001 From: David Cooper Date: Thu, 28 Jun 2018 16:17:04 -0400 Subject: [PATCH 2/2] Handle incorrectly populated certificate_list According to Section 7.4.2 of RFC 5246, when a server sends its certificate it MUST send a list in which the first certificate is the sender's certificate and "Each following certificate MUST directly certify the one preceding it." testssl.sh currently assumes that the server has populated the list way and so places the second certificate in the list into $TEMPDIR/hostcert_issuer.pem. However, not all servers have been following this requirement, and so draft-ietf-tls-tls13 (soon to be RFC 8446) only says that servers SHOULD list the certificates in this way and says that clients "SHOULD be prepared to handle potentially extraneous certificates and arbitrary orderings from any TLS version, with the exception of the end-entity certificate which MUST be first." testssl.sh needs to place the correct certificate in $TEMPDIR/hostcert_issuer.pem, since otherwise any OCSP request it sends will be incorrect, and any attempt to verify and OCSP response will be incorrect as well. This PR changes extract_certificates() and parse_tls_serverhello() to populate $TEMPDIR/hostcert_issuer.pem with the first certificate in certificate_list that has a subject DN that matches the issuer DN in the server's certificate, rather than simply populating $TEMPDIR/hostcert_issuer.pem with the second certificate in the list. In testing a random sampling of U.S. government servers, of 57 servers tested 5 reported "unauthorized" for the OCSP URI using the current testssl.sh and all 5 of these reported "not revoked" with this PR. This PR also corrects the same issue in some servers on the Alexa Top 1000, but this was only a problem for 12 of those 1000 servers. --- testssl.sh | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/testssl.sh b/testssl.sh index 816f616..c944201 100755 --- a/testssl.sh +++ b/testssl.sh @@ -6555,7 +6555,8 @@ determine_tls_extensions() { extract_certificates() { local version="$1" local savedir - local -i success nrsaved=0 + local -i i success nrsaved=0 + local issuerDN CAsubjectDN # Place the server's certificate in $HOSTCERT and any intermediate # certificates that were provided in $TEMPDIR/intermediatecerts.pem @@ -6582,7 +6583,26 @@ extract_certificates() { echo "" > $TEMPDIR/intermediatecerts.pem else cat level?.crt > $TEMPDIR/intermediatecerts.pem - cp level1.crt $TEMPDIR/hostcert_issuer.pem + issuerDN="$($OPENSSL x509 -in $HOSTCERT -noout -issuer 2>/dev/null)" + issuerDN="${issuerDN:8}" + # The second certficate (level1.crt) SHOULD be issued to the CA + # that issued the server's certificate. But, according to RFC 8446 + # clients SHOULD be prepared to handle cases in which the server + # does not order the certificates correctly. + for (( i=1; i < nrsaved; i++ )); do + CAsubjectDN="$($OPENSSL x509 -in "level$i.crt" -noout -subject 2>/dev/null)" + if [[ "${CAsubjectDN:9}" == "$issuerDN" ]]; then + cp "level$i.crt" $TEMPDIR/hostcert_issuer.pem + break + fi + done + # This should never happen, but if more than one certificate was + # provided and none of them belong to the CA that issued the + # server's certificate, then the extra certificates should just + # be deleted. There is code elsewhere that assumes that if + # $TEMPDIR/intermediatecerts.pem is non-empty, then + # $TEMPDIR/hostcert_issuer.pem is also present. + [[ $i -eq $nrsaved ]] && echo "" > $TEMPDIR/intermediatecerts.pem rm level?.crt fi fi @@ -11097,7 +11117,7 @@ parse_tls_serverhello() { echo " i:${CAissuerDN:8}" >> $TMPFILE echo "$pem_certificate" >> $TMPFILE echo "$pem_certificate" >> "$TEMPDIR/intermediatecerts.pem" - if [[ -z "$hostcert_issuer" ]]; then + if [[ -z "$hostcert_issuer" ]] && [[ "${CAsubjectDN:9}" == "${issuerDN:8}" ]]; then # The issuer's certificate is needed if there is a stapled OCSP response, # and it may be needed if check_revocation_ocsp() will later be called # with the OCSP URI in the server's certificate.