From 0602963cd52b4a7e8be3e3cd476c32740342a817 Mon Sep 17 00:00:00 2001 From: Odinmylord Date: Fri, 22 Mar 2024 17:48:39 +0100 Subject: [PATCH 01/12] Add support for signature_algorithms_cert extension --- testssl.sh | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/testssl.sh b/testssl.sh index f09339605..693650d4f 100755 --- a/testssl.sh +++ b/testssl.sh @@ -10356,6 +10356,12 @@ run_server_defaults() { i+=1 done <<< "$CLIENT_AUTH_CA_LIST" fi + pr_bold " Offered Signature Algorithms " + outln "$sigalgs_client" + fileout "offered_client_certificate_sigalgs" "INFO" "$sigalgs_client" + pr_bold " Shared Signature Algorithms " + outln "$sigalgs_client_shared" + fileout "shared_client_certificate_sigalgs" "INFO" "$sigalgs_client_shared" fi @@ -21663,6 +21669,27 @@ extract_calist() { return 0 } +# Given the OpenSSL output of a response from a TLS server (with the -msg option) +# in which the response includes a CertificateRequest message, return the list of +# signature algorithms supported by the server. +extract_sigalgs_client() { + local response="$1" + local sigalgslist sigalgs="" sigalgs_shared="" + if [[ "$response" =~ Requested\ Signature\ Algorithms ]]; then + # The structure is: + # Requested Signature Algorithms: algs + # Shared Requested Signature Algorithms: algs + # Peer signing digest: digest + sigalgslist="${response#*Requested Signature Algorithms: }" + sigalgs="${sigalgslist%*Shared*}" + sigalgs_shared="${sigalgslist##*Requested Signature Algorithms: }" + sigalgs_shared="${sigalgs_shared%*Peer*}" + fi + # both are important since there is no guarantee that the client accepts all the signature algorithms + local result=("$sigalgs" "$sigalgs_shared") + echo ${result[@]}; +} + # This is only being called from determine_optimal_proto() in order to check whether we have a server with # client authentication, a server with no SSL session ID switched off -- and as the name indicates a protocol. # ARG1 is the openssl s_client connect return value. (Darwin or LibreSSL may return 1 here) @@ -21691,6 +21718,9 @@ sclient_auth() { CLIENT_AUTH="required" [[ $1 -eq 0 ]] && CLIENT_AUTH="optional" CLIENT_AUTH_CA_LIST="$(extract_calist "$server_hello")" + local sigalgs=($(extract_sigalgs_client "$server_hello")) + sigalgs_client=${sigalgs[0]} + sigalgs_client_shared=${sigalgs[1]} return 0 fi [[ $1 -eq 0 ]] && return 0 From 156d82ee097f8571588a5be8021915c57aba36db Mon Sep 17 00:00:00 2001 From: Odinmylord Date: Tue, 16 Apr 2024 11:42:41 +0200 Subject: [PATCH 02/12] Added support for signature_algorithms extensions in CertificateRequest. Still missing the check for both TLS1.2 and TLS1.3 --- testssl.sh | 122 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 85 insertions(+), 37 deletions(-) diff --git a/testssl.sh b/testssl.sh index e835186f5..638ea7198 100755 --- a/testssl.sh +++ b/testssl.sh @@ -288,6 +288,8 @@ TMPFILE="" ERRFILE="" CLIENT_AUTH="none" CLIENT_AUTH_CA_LIST="" +CLIENT_AUTH_SIGALGS_LIST="" +CLIENT_AUTH_SIGALGS_CERT_LIST="" TLS_TICKETS=false NO_SSL_SESSIONID=true CERT_COMPRESSION=${CERT_COMPRESSION:-false} # secret flag to set in addition to --devel for certificate compression @@ -10356,12 +10358,16 @@ run_server_defaults() { i+=1 done <<< "$CLIENT_AUTH_CA_LIST" fi + jsonID="clientAuth_Signature_Algorithms" pr_bold " Offered Signature Algorithms " - outln "$sigalgs_client" - fileout "offered_client_certificate_sigalgs" "INFO" "$sigalgs_client" - pr_bold " Shared Signature Algorithms " - outln "$sigalgs_client_shared" - fileout "shared_client_certificate_sigalgs" "INFO" "$sigalgs_client_shared" + out_row_aligned "$CLIENT_AUTH_SIGALGS_LIST" + fileout "$jsonID" "INFO" "$CLIENT_AUTH_SIGALGS_LIST" + jsonID="clientAuth_Signature_Algorithms_Cert " + if [[ "$CLIENT_AUTH_SIGALGS_CERT_LIST" != empty\ ]] ; then + pr_bold " Offered Signature Algorithms for Certificates " + out_row_aligned "$CLIENT_AUTH_SIGALGS_CERT_LIST" + fileout "$jsonID" "INFO" "$CLIENT_AUTH_SIGALGS_CERT_LIST" + fi fi @@ -21598,13 +21604,15 @@ print_dn() { } # Given the OpenSSL output of a response from a TLS server (with the -msg option) -# in which the response includes a CertificateRequest message, return the list of -# distinguished names that are in the CA list. +# in which the response includes a CertificateRequest message, update the CLIENT_AUTH_CA_LIST, +# CLIENT_AUTH_SIGALGS_LIST and CLIENT_AUTH_SIGALGS_CERT_LIST variables with data from the message. extract_calist() { local response="$1" local is_tls12=false is_tls13=false - local certreq calist="" certtypes sigalgs dn + local certreq calist="" certtypes sigalgs sigalgs_cert dn local calist_string="" + local sigalgs_string="" + local sigalgs_string_cert="" local -i len type # Determine whether this is a TLS 1.2 or TLS 1.3 response, since the information @@ -21637,12 +21645,25 @@ extract_calist() { [[ -z "$certreq" ]] && break type=$(hex2dec "${certreq:0:4}") len=2*$(hex2dec "${certreq:4:4}") - if [[ $type -eq 47 ]]; then + if [[ $type -eq 13 ]]; then + # This is the signature_algorithms extension + # First two bytes are the extension type, the next two bytes are the length of the extension + sigalgs="${certreq:8:len}" + # The variable name is el_len so that it does not overwrite the len of the whole extension + el_len=2*$(hex2dec "${sigalgs:0:4}") + # Since the structure of this extension only has one element in it, we can take everything + # after the two bytes which contain the length of the element. + sigalgs="${sigalgs:4:el_len}" + elif [[ $type -eq 47 ]]; then # This is the certificate_authorities extension calist="${certreq:8:len}" - len=2*$(hex2dec "${calist:0:4}") - calist="${calist:4:len}" - break + el_len=2*$(hex2dec "${calist:0:4}") + calist="${calist:4:el_len}" + elif [[ $type -eq 50 ]]; then + # This is the signature_algorithms_cert extension + sigalgs_cert="${certreq:8:len}" + el_len=2*$(hex2dec "${sigalgs_cert:0:4}") + sigalgs_cert="${sigalgs_cert:4:el_len}" fi certreq="${certreq:$((len+8))}" done @@ -21673,29 +21694,59 @@ extract_calist() { calist="${calist:$((len+4))}" done [[ -z "$calist_string" ]] && calist_string="empty" - tm_out "$calist_string" + CLIENT_AUTH_CA_LIST="$(safe_echo "$calist_string")" + sigalgs_string="$(sigalgs_converter "$sigalgs")" + CLIENT_AUTH_SIGALGS_LIST="${sigalgs_string} " + [[ -z "$sigalgs_string" ]] && CLIENT_AUTH_SIGALGS_LIST="empty " + sigalgs_string_cert="$(sigalgs_converter "$sigalgs_cert")" + CLIENT_AUTH_SIGALGS_CERT_LIST="${sigalgs_string_cert} " + [[ -z "$sigalgs_string_cert" ]] && CLIENT_AUTH_SIGALGS_CERT_LIST="empty " return 0 } - -# Given the OpenSSL output of a response from a TLS server (with the -msg option) -# in which the response includes a CertificateRequest message, return the list of -# signature algorithms supported by the server. -extract_sigalgs_client() { - local response="$1" - local sigalgslist sigalgs="" sigalgs_shared="" - if [[ "$response" =~ Requested\ Signature\ Algorithms ]]; then - # The structure is: - # Requested Signature Algorithms: algs - # Shared Requested Signature Algorithms: algs - # Peer signing digest: digest - sigalgslist="${response#*Requested Signature Algorithms: }" - sigalgs="${sigalgslist%*Shared*}" - sigalgs_shared="${sigalgslist##*Requested Signature Algorithms: }" - sigalgs_shared="${sigalgs_shared%*Peer*}" - fi - # both are important since there is no guarantee that the client accepts all the signature algorithms - local result=("$sigalgs" "$sigalgs_shared") - echo ${result[@]}; +# Given the list of signature algorithms in hex format (no space) take each four +# characters group and convert it to the corresponding signature algorithm. +sigalgs_converter() { + local sigalgs=$1 + local sigalgs_string="" + while true; do + [[ -z "$sigalgs" ]] && break + case "${sigalgs:0:4}" in + 0101) sigalgs_string+=" RSA+MD5" ;; + 0102) sigalgs_string+=" DSA+MD5" ;; + 0103) sigalgs_string+=" ECDSA+MD5" ;; + 0201) sigalgs_string+=" RSA+SHA1" ;; + 0202) sigalgs_string+=" DSA+SHA1" ;; + 0203) sigalgs_string+=" ECDSA+SHA1" ;; + 0301) sigalgs_string+=" RSA+SHA224" ;; + 0302) sigalgs_string+=" DSA+SHA224" ;; + 0303) sigalgs_string+=" ECDSA+SHA224" ;; + 0401|0420) sigalgs_string+=" RSA+SHA256" ;; + 0402) sigalgs_string+=" DSA+SHA256" ;; + 0403) sigalgs_string+=" ECDSA+SHA256" ;; + 0501|0520) sigalgs_string+=" RSA+SHA384" ;; + 0502) sigalgs_string+=" DSA+SHA384" ;; + 0503) sigalgs_string+=" ECDSA+SHA384" ;; + 0601|0620) sigalgs_string+=" RSA+SHA512" ;; + 0602) sigalgs_string+=" DSA+SHA512" ;; + 0603) sigalgs_string+=" ECDSA+SHA512" ;; + 0708) sigalgs_string+=" SM2+SM3" ;; + 0804) sigalgs_string+=" RSA-PSS-RSAE+SHA256" ;; + 0805) sigalgs_string+=" RSA-PSS-RSAE+SHA384" ;; + 0806) sigalgs_string+=" RSA-PSS-RSAE+SHA512" ;; + 0807) sigalgs_string+=" Ed25519" ;; + 0808) sigalgs_string+=" Ed448" ;; + 0809) sigalgs_string+=" RSA-PSS-PSS+SHA256" ;; + 080a) sigalgs_string+=" RSA-PSS-PSS+SHA384" ;; + 080b) sigalgs_string+=" RSA-PSS-PSS+SHA512" ;; + 081a) sigalgs_string+=" ECDSA-BRAINPOOL+SHA256" ;; + 081b) sigalgs_string+=" ECDSA-BRAINPOOL+SHA384" ;; + 081c) sigalgs_string+=" ECDSA-BRAINPOOL+SHA512" ;; + *) sigalgs_string+=" unknown(${sigalgs:0:4})";; + esac + sigalgs="${sigalgs:4}" + done + echo $sigalgs_string + return 0 } # This is only being called from determine_optimal_proto() in order to check whether we have a server with @@ -21725,10 +21776,7 @@ sclient_auth() { # CertificateRequest message in -msg CLIENT_AUTH="required" [[ $1 -eq 0 ]] && CLIENT_AUTH="optional" - CLIENT_AUTH_CA_LIST="$(extract_calist "$server_hello")" - local sigalgs=($(extract_sigalgs_client "$server_hello")) - sigalgs_client=${sigalgs[0]} - sigalgs_client_shared=${sigalgs[1]} + extract_calist "$server_hello" return 0 fi [[ $1 -eq 0 ]] && return 0 From 1c94b064ca64065290d10ab4d12e563ccd1392db Mon Sep 17 00:00:00 2001 From: Riccardo Germenia Date: Mon, 20 May 2024 13:49:52 +0200 Subject: [PATCH 03/12] Add support for CertificateRequest message to tlssockets --- testssl.sh | 82 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 63 insertions(+), 19 deletions(-) diff --git a/testssl.sh b/testssl.sh index 638ea7198..06b838a2b 100755 --- a/testssl.sh +++ b/testssl.sh @@ -13950,9 +13950,10 @@ parse_tls_serverhello() { local tls_serverhello_ascii="" tls_certificate_ascii="" local tls_serverkeyexchange_ascii="" tls_certificate_status_ascii="" local tls_encryptedextensions_ascii="" tls_revised_certificate_msg="" + local tls_certificate_request_ascii="" local -i tls_serverhello_ascii_len=0 tls_certificate_ascii_len=0 local -i tls_serverkeyexchange_ascii_len=0 tls_certificate_status_ascii_len=0 - local -i tls_encryptedextensions_ascii_len=0 + local -i tls_encryptedextensions_ascii_len=0 tls_certificate_request_ascii_len=0 local added_encrypted_extensions=false local tls_alert_descrip tls_sid_len_hex issuerDN subjectDN CAissuerDN CAsubjectDN local -i tls_sid_len offset extns_offset nr_certs=0 @@ -14205,6 +14206,14 @@ parse_tls_serverhello() { fi tls_serverkeyexchange_ascii="${tls_handshake_ascii:i:msg_len}" tls_serverkeyexchange_ascii_len=$msg_len + elif [[ "$process_full" =~ all ]] && [[ "$tls_msg_type" == 0D ]]; then + if [[ -n "$tls_certificate_request_ascii" ]]; then + debugme tmln_warning "Response contained more than one CertificateRequest handshake message." + [[ $DEBUG -ge 1 ]] && tmpfile_handle ${FUNCNAME[0]}.txt + return 1 + fi + tls_certificate_request_ascii="${tls_handshake_ascii:i:msg_len}" + tls_certificate_request_ascii_len=$msg_len elif [[ "$tls_msg_type" == 0F ]]; then if [[ $msg_len -lt 4 ]]; then debugme tmln_warning "Response contained malformed certificate_verify message." @@ -14814,6 +14823,20 @@ parse_tls_serverhello() { fi done fi + # Now parse the CertificateRequest message. + if [[ $tls_certificate_request_ascii_len -ne 0 ]] && [[ "$process_full" =~ all ]]; then + # The CertificateRequest message is only present in TLS 1.3 and it has a 2 char identifier and a 6 char length. + if [[ $tls_certificate_request_ascii_len -lt 8 ]]; then + debugme echo "Malformed CertificateRequest Handshake message in ServerHello." + tmpfile_handle ${FUNCNAME[0]}.txt + return 1 + fi + # The extract_calist can be used to extract the extensions' data from the CertificateRequest message. + # The second parameter is the TLS version, if it is provided extract_calist does not try to get it. + extract_calist "$tls_certificate_request_ascii" "${DETECTED_TLS_VERSION:2:2}" + # not sure about this + CLIENT_AUTH="required" + fi # Now parse the Certificate message. if [[ "$process_full" =~ all ]]; then @@ -21606,8 +21629,17 @@ print_dn() { # Given the OpenSSL output of a response from a TLS server (with the -msg option) # in which the response includes a CertificateRequest message, update the CLIENT_AUTH_CA_LIST, # CLIENT_AUTH_SIGALGS_LIST and CLIENT_AUTH_SIGALGS_CERT_LIST variables with data from the message. +# The second_parameter is optional and should contain the two low bytes of the TLS version. +# If the second parameter is provided the first parameter should contain the certreq in the following format: +# - hex format +# - no spaces or new lines characters +# - first 8 characters removed +# - the first 2 characters are the message identifier +# - the other 6 are the length of the whole message +# The tls_socket function already provides the message in this format. extract_calist() { local response="$1" + local tls_version="$2" local is_tls12=false is_tls13=false local certreq calist="" certtypes sigalgs sigalgs_cert dn local calist_string="" @@ -21615,28 +21647,34 @@ extract_calist() { local sigalgs_string_cert="" local -i len type - # Determine whether this is a TLS 1.2 or TLS 1.3 response, since the information - # is encoded in a different place for TLS 1.3 and the CertificateRequest message - # differs between TLS 1.2 and TLS 1.1 and earlier. - if [[ "$response" =~ \<\<\<\ TLS\ 1.3[\,]?\ Handshake\ \[length\ [0-9a-fA-F]*\]\,\ CertificateRequest ]]; then - is_tls13=true - elif [[ "$response" =~ \<\<\<\ TLS\ 1.2[\,]?\ Handshake\ \[length\ [0-9a-fA-F]*\]\,\ CertificateRequest ]]; then - is_tls12=true - fi # Extract just the CertificateRequest message as an ASCII-HEX string. - certreq="${response##*CertificateRequest}" - certreq="0d${certreq#*0d}" - certreq="${certreq%%<<<*}" - certreq="$(strip_spaces "$(newline_to_spaces "$certreq")")" - certreq="${certreq:8}" - - # Get the list of DNs from the CertificateRequest message. - if "$is_tls13"; then + # The tls_version variable on ly exists if this function is called from tls_sockets which provides + # certreq in the right format. + if [[ -z "$tls_version" ]] then + # Determine whether this is a TLS 1.2 or TLS 1.3 response, since the information + # is encoded in a different place for TLS 1.3 and the CertificateRequest message + # differs between TLS 1.2 and TLS 1.1 and earlier. + if [[ "$response" =~ \<\<\<\ TLS\ 1.3[\,]?\ Handshake\ \[length\ [0-9a-fA-F]*\]\,\ CertificateRequest ]]; then + is_tls13=true + elif [[ "$response" =~ \<\<\<\ TLS\ 1.2[\,]?\ Handshake\ \[length\ [0-9a-fA-F]*\]\,\ CertificateRequest ]]; then + is_tls12=true + fi + certreq="${response##*CertificateRequest}" + certreq="0d${certreq#*0d}" + certreq="${certreq%%<<<*}" + certreq="$(strip_spaces "$(newline_to_spaces "$certreq")")" + certreq="${certreq:8}" + else + certreq="$response" + fi + # Extract the extensions' information + if "$is_tls13" || [[ 0x"$tls_version" = "0x04" ]]; then # struct { # opaque certificate_request_context<0..2^8-1>; # Extension extensions<2..2^16-1>; # } CertificateRequest; + # Context len len=2*$(hex2dec "${certreq:0:2}") certreq="${certreq:$((len+2))}" len=2*$(hex2dec "${certreq:0:4}") @@ -21707,6 +21745,7 @@ extract_calist() { # characters group and convert it to the corresponding signature algorithm. sigalgs_converter() { local sigalgs=$1 + sigalgs="$(echo "$sigalgs" | tr '[:upper:]' '[:lower:]')" local sigalgs_string="" while true; do [[ -z "$sigalgs" ]] && break @@ -21965,7 +22004,7 @@ determine_optimal_sockets_params() { determine_optimal_proto() { local all_failed=true local tmp="" - local proto optimal_proto + local proto optimal_proto actual_proto local jsonID="optimal_proto" "$do_tls_sockets" && return 0 @@ -22133,8 +22172,13 @@ determine_optimal_proto() { ignore_no_or_lame " Type \"yes\" to proceed and accept false negatives or positives" "yes" [[ $? -ne 0 ]] && exit $ERR_CLUELESS fi - + actual_proto="$(get_protocol $TMPFILE)" tmpfile_handle ${FUNCNAME[0]}.txt + if [[ $actual_proto == TLSv1.3 ]] && [[ "$(has_server_protocol "tls1_2")" -eq 0 ]] && [[ "$CLIENT_AUTH" != none ]]; then + # In this case we need a TLS1.2 handshake to get the information for both TLS1.2 and TLS1.3 + $OPENSSL s_client $(s_client_options "-tls1_2 $BUGS -connect "$NODEIP:$PORT" -msg $PROXY $SNI") $TMPFILE 2>>$ERRFILE + tmpfile_handle "tls12_handshake.txt" + fi return 0 } From 1c6f1a4f00f1da9c6c3bddba8c2ecccdf39a7fc0 Mon Sep 17 00:00:00 2001 From: Odinmylord Date: Mon, 20 May 2024 14:32:35 +0200 Subject: [PATCH 04/12] fix missing semicolon --- testssl.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testssl.sh b/testssl.sh index 06b838a2b..f0c246139 100755 --- a/testssl.sh +++ b/testssl.sh @@ -21651,7 +21651,7 @@ extract_calist() { # Extract just the CertificateRequest message as an ASCII-HEX string. # The tls_version variable on ly exists if this function is called from tls_sockets which provides # certreq in the right format. - if [[ -z "$tls_version" ]] then + if [[ -z "$tls_version" ]]; then # Determine whether this is a TLS 1.2 or TLS 1.3 response, since the information # is encoded in a different place for TLS 1.3 and the CertificateRequest message # differs between TLS 1.2 and TLS 1.1 and earlier. From c084d0b552da8a7c00785f576707cd90921e0f7c Mon Sep 17 00:00:00 2001 From: Odinmylord Date: Thu, 30 May 2024 14:35:53 +0200 Subject: [PATCH 05/12] fixed issue with TLS1.2 --- testssl.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/testssl.sh b/testssl.sh index f0c246139..9acb9bcb1 100755 --- a/testssl.sh +++ b/testssl.sh @@ -14825,7 +14825,7 @@ parse_tls_serverhello() { fi # Now parse the CertificateRequest message. if [[ $tls_certificate_request_ascii_len -ne 0 ]] && [[ "$process_full" =~ all ]]; then - # The CertificateRequest message is only present in TLS 1.3 and it has a 2 char identifier and a 6 char length. + # The CertificateRequest message is only present in TLS 1.3 and TLS 1.2, it has a 2 char identifier and a 6 char length. if [[ $tls_certificate_request_ascii_len -lt 8 ]]; then debugme echo "Malformed CertificateRequest Handshake message in ServerHello." tmpfile_handle ${FUNCNAME[0]}.txt @@ -14834,8 +14834,8 @@ parse_tls_serverhello() { # The extract_calist can be used to extract the extensions' data from the CertificateRequest message. # The second parameter is the TLS version, if it is provided extract_calist does not try to get it. extract_calist "$tls_certificate_request_ascii" "${DETECTED_TLS_VERSION:2:2}" - # not sure about this - CLIENT_AUTH="required" + # Can not find a way to check if it is optional or required + CLIENT_AUTH="optional" fi # Now parse the Certificate message. @@ -21649,7 +21649,7 @@ extract_calist() { # Extract just the CertificateRequest message as an ASCII-HEX string. - # The tls_version variable on ly exists if this function is called from tls_sockets which provides + # The tls_version variable only exists if this function is called from tls_sockets which provides # certreq in the right format. if [[ -z "$tls_version" ]]; then # Determine whether this is a TLS 1.2 or TLS 1.3 response, since the information @@ -21715,7 +21715,7 @@ extract_calist() { len=2*$(hex2dec "${certreq:0:2}") certtypes="${certreq:2:len}" certreq="${certreq:$((len+2))}" - if "$is_tls12"; then + if "$is_tls12" || [[ 0x"$tls_version" = "0x03" ]]; then len=2*$(hex2dec "${certreq:0:4}") sigalgs="${certreq:4:len}" certreq="${certreq:$((len+4))}" From 6b19201c6ba143f88f4fec3c0579d0ce20285758 Mon Sep 17 00:00:00 2001 From: Odinmylord Date: Fri, 28 Jun 2024 16:15:56 +0200 Subject: [PATCH 06/12] Added signature_algorithms_cert retrieval for both TLS1.2 and TLS1.3 --- testssl.sh | 60 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 40 insertions(+), 20 deletions(-) diff --git a/testssl.sh b/testssl.sh index 9acb9bcb1..b25eb2133 100755 --- a/testssl.sh +++ b/testssl.sh @@ -288,8 +288,9 @@ TMPFILE="" ERRFILE="" CLIENT_AUTH="none" CLIENT_AUTH_CA_LIST="" -CLIENT_AUTH_SIGALGS_LIST="" -CLIENT_AUTH_SIGALGS_CERT_LIST="" +CLIENT_AUTH_SIGALGS_LIST_13="empty" +CLIENT_AUTH_SIGALGS_CERT_LIST_13="empty" +CLIENT_AUTH_SIGALGS_LIST_12="empty" TLS_TICKETS=false NO_SSL_SESSIONID=true CERT_COMPRESSION=${CERT_COMPRESSION:-false} # secret flag to set in addition to --devel for certificate compression @@ -10358,16 +10359,30 @@ run_server_defaults() { i+=1 done <<< "$CLIENT_AUTH_CA_LIST" fi - jsonID="clientAuth_Signature_Algorithms" - pr_bold " Offered Signature Algorithms " - out_row_aligned "$CLIENT_AUTH_SIGALGS_LIST" - fileout "$jsonID" "INFO" "$CLIENT_AUTH_SIGALGS_LIST" - jsonID="clientAuth_Signature_Algorithms_Cert " - if [[ "$CLIENT_AUTH_SIGALGS_CERT_LIST" != empty\ ]] ; then + if [[ "$CLIENT_AUTH_SIGALGS_LIST_13" = "$CLIENT_AUTH_SIGALGS_LIST_12" ]]; then + pr_bold " Offered Signature Algorithms " + out_row_aligned "$CLIENT_AUTH_SIGALGS_LIST_13" + else + if [[ $CLIENT_AUTH_SIGALGS_LIST_13 != empty ]]; then + pr_bold " Offered Signature Algorithms (TLS 1.3) " + out_row_aligned "$CLIENT_AUTH_SIGALGS_LIST_13" + fi + if [[ $CLIENT_AUTH_SIGALGS_LIST_12 != empty ]]; then + pr_bold " Offered Signature Algorithms (TLS 1.2) " + out_row_aligned "$CLIENT_AUTH_SIGALGS_LIST_12" + fi + fi + jsonID="clientAuth_Signature_Algorithms_TLS13" + fileout "$jsonID" "INFO" "$CLIENT_AUTH_SIGALGS_LIST_13" + jsonID="clientAuth_Signature_Algorithms_TLS12" + fileout "$jsonID" "INFO" "$CLIENT_AUTH_SIGALGS_LIST_12" + + if [[ "$CLIENT_AUTH_SIGALGS_CERT_LIST_13" != empty ]]; then pr_bold " Offered Signature Algorithms for Certificates " - out_row_aligned "$CLIENT_AUTH_SIGALGS_CERT_LIST" - fileout "$jsonID" "INFO" "$CLIENT_AUTH_SIGALGS_CERT_LIST" + out_row_aligned "$CLIENT_AUTH_SIGALGS_CERT_LIST_13" fi + jsonID="clientAuth_Signature_Algorithms_Cert_TLS13" + fileout "$jsonID" "INFO" "$CLIENT_AUTH_SIGALGS_CERT_LIST_13" fi @@ -21705,6 +21720,12 @@ extract_calist() { fi certreq="${certreq:$((len+8))}" done + sigalgs_string="$(sigalgs_converter "$sigalgs")" + CLIENT_AUTH_SIGALGS_LIST_13="${sigalgs_string}" + [[ -z "$sigalgs_string" ]] && CLIENT_AUTH_SIGALGS_LIST_13="empty" + sigalgs_string_cert="$(sigalgs_converter "$sigalgs_cert")" + CLIENT_AUTH_SIGALGS_CERT_LIST_13="${sigalgs_string_cert}" + [[ -z "$sigalgs_string_cert" ]] && CLIENT_AUTH_SIGALGS_CERT_LIST_13="empty" else # struct { # ClientCertificateType certificate_types<1..2^8-1>; @@ -21722,6 +21743,9 @@ extract_calist() { fi len=2*$(hex2dec "${certreq:0:4}") calist="${certreq:4:len}" + sigalgs_string="$(sigalgs_converter "$sigalgs")" + CLIENT_AUTH_SIGALGS_LIST_12="${sigalgs_string}" + [[ -z "$sigalgs_string" ]] && CLIENT_AUTH_SIGALGS_LIST_12="empty" fi # Convert each DN to a string. while true; do @@ -21733,12 +21757,6 @@ extract_calist() { done [[ -z "$calist_string" ]] && calist_string="empty" CLIENT_AUTH_CA_LIST="$(safe_echo "$calist_string")" - sigalgs_string="$(sigalgs_converter "$sigalgs")" - CLIENT_AUTH_SIGALGS_LIST="${sigalgs_string} " - [[ -z "$sigalgs_string" ]] && CLIENT_AUTH_SIGALGS_LIST="empty " - sigalgs_string_cert="$(sigalgs_converter "$sigalgs_cert")" - CLIENT_AUTH_SIGALGS_CERT_LIST="${sigalgs_string_cert} " - [[ -z "$sigalgs_string_cert" ]] && CLIENT_AUTH_SIGALGS_CERT_LIST="empty " return 0 } # Given the list of signature algorithms in hex format (no space) take each four @@ -22174,10 +22192,12 @@ determine_optimal_proto() { fi actual_proto="$(get_protocol $TMPFILE)" tmpfile_handle ${FUNCNAME[0]}.txt - if [[ $actual_proto == TLSv1.3 ]] && [[ "$(has_server_protocol "tls1_2")" -eq 0 ]] && [[ "$CLIENT_AUTH" != none ]]; then - # In this case we need a TLS1.2 handshake to get the information for both TLS1.2 and TLS1.3 - $OPENSSL s_client $(s_client_options "-tls1_2 $BUGS -connect "$NODEIP:$PORT" -msg $PROXY $SNI") $TMPFILE 2>>$ERRFILE - tmpfile_handle "tls12_handshake.txt" + # if the actual_proto is TLSv1.2 and TLSv1.3 is available it probably means that the + # current openssl version does not support TLSv1.3 so tls_sockets must be used to + # obtain information about the certificate request + if [[ $actual_proto == TLSv1.2 ]] && [[ "$(has_server_protocol "tls1_3")" -eq 0 ]] && [[ "$CLIENT_AUTH" != none ]]; then + # this call performs the handshake and retrieves the signature_algorithms_cert information + tls_sockets "04" "$TLS13_CIPHER" "all+" "" "" false fi return 0 } From 0c279554be243d8f58d12b8821e92e575af8566e Mon Sep 17 00:00:00 2001 From: Odinmylord Date: Tue, 16 Jul 2024 11:45:51 +0200 Subject: [PATCH 07/12] Fix small issue with CLIENT_AUTH --- testssl.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testssl.sh b/testssl.sh index 222f84392..b1637778f 100755 --- a/testssl.sh +++ b/testssl.sh @@ -14850,7 +14850,7 @@ parse_tls_serverhello() { # The second parameter is the TLS version, if it is provided extract_calist does not try to get it. extract_calist "$tls_certificate_request_ascii" "${DETECTED_TLS_VERSION:2:2}" # Can not find a way to check if it is optional or required - CLIENT_AUTH="optional" + [[ "$CLIENT_AUTH" = none ]] && CLIENT_AUTH="optional" fi # Now parse the Certificate message. From 339ebf0d963c77769114e3abc26a1fdf5d5cd7b2 Mon Sep 17 00:00:00 2001 From: Riccardo Germenia Date: Mon, 2 Sep 2024 11:39:17 +0200 Subject: [PATCH 08/12] Add check for "required" client certificate to tls_sockets --- testssl.sh | 43 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/testssl.sh b/testssl.sh index bf5492b36..e4d8cb4ac 100755 --- a/testssl.sh +++ b/testssl.sh @@ -16074,7 +16074,8 @@ tls_sockets() { local close_connection=true include_headers=true local -i i len msg_len tag_len hello_done=0 seq_num=0 local cipher="" tls_version handshake_secret="" res - local initial_msg_transcript msg_transcript finished_msg aad="" data="" plaintext + local initial_msg_transcript msg_transcript finished_msg aad="" data="" cert_msg="" plaintext + local server_key server_iv server_finished_key local handshake_traffic_keys key iv finished_key local master_secret master_traffic_keys @@ -16253,7 +16254,7 @@ tls_sockets() { if ! "$close_connection" && [[ $save == 0 ]] && \ [[ -n "$handshake_secret" ]] && [[ "$process_full" == all+ ]]; then - tls_version="$DETECTED_TLS_VERSION" + tls_version="$DETECTED_TLS_VERSION" if [[ "${TLS_SERVER_HELLO:8:3}" == 7F1 ]]; then tls_version="${TLS_SERVER_HELLO:8:4}" elif [[ "$TLS_SERVER_HELLO" =~ 002B00027F1[0-9A-F] ]]; then @@ -16263,12 +16264,50 @@ tls_sockets() { handshake_traffic_keys="$(derive-handshake-traffic-keys "$cipher" "$handshake_secret" "$initial_msg_transcript" "client")" read -r key iv finished_key <<< "$handshake_traffic_keys" + [[ -z master_secret ]] && master_secret="$(derive-master-secret "$cipher" "$handshake_secret")" + master_traffic_keys="$(derive-application-traffic-keys "$cipher" "$master_secret" "$msg_transcript" server)" + read -r server_key server_iv server_finished_key <<< "$master_traffic_keys" if [[ "$cipher" == *SHA256 ]]; then finished_msg="14000020$(hmac-transcript "-sha256" "$finished_key" "$msg_transcript")" else finished_msg="14000030$(hmac-transcript "-sha384" "$finished_key" "$msg_transcript")" fi [[ "$cipher" =~ CCM_8 ]] && tag_len=8 || tag_len=16 + if [[ $CLIENT_AUTH == optional ]]; then + # in this case we try to send a client certificate + change_cipher_spec="140303000101" + len=${#change_cipher_spec} + for (( i=0; i < len; i+=2 )); do + data+=", ${change_cipher_spec:i:2}" + done + socksend_clienthello "${data}" + data="" + # empty certificate message 0b 00 00 04 00 00 00 00 this is used only to know whether the Certificate + # is required or optional. + cert_msg="0b00000400000000" + aad="170303$(printf "%04X" "$(( ${#cert_msg}/2 + tag_len + 1 ))")" + if "$include_headers"; then + cert_msg="$(sym-encrypt "$cipher" "$key" "$(get-nonce "$iv" 0)" "${cert_msg}16" "$aad")" + else + cert_msg="$(sym-encrypt "$cipher" "$key" "$(get-nonce "$iv" 0)" "${cert_msg}16" "")" + fi + cert_msg="$aad$cert_msg" + len=${#cert_msg} + for (( i=0; i < len; i+=2 )); do + data+=", ${cert_msg:i:2}" + done + debugme echo -e "\nsending certificate..." + socksend_clienthello "${data}" + data="" + + sockread 32768 + ciphertext=$(hexdump -v -e '16/1 "%02X"' "$SOCK_REPLY_FILE") + msg_len=$((2*0x${ciphertext:6:4})) + aad="${ciphertext:0:10}" + "$include_headers" || aad="" + data="$(sym-decrypt "$cipher" "$server_key" "$(get-nonce "$server_iv" 0)" "${ciphertext:10:msg_len}" "$aad")" + [[ $data == 027415 ]] && CLIENT_AUTH=required + fi aad="170303$(printf "%04X" "$(( ${#finished_msg}/2 + tag_len + 1 ))")" if "$include_headers"; then # The header information was added to additional data in TLSv1.3 draft 25. From 98b7e42d3a6958ee71389a451ffff394f06d3f69 Mon Sep 17 00:00:00 2001 From: Riccardo Germenia Date: Mon, 2 Sep 2024 14:25:40 +0200 Subject: [PATCH 09/12] fix small issue --- testssl.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testssl.sh b/testssl.sh index e4d8cb4ac..f76cbf26b 100755 --- a/testssl.sh +++ b/testssl.sh @@ -16254,7 +16254,7 @@ tls_sockets() { if ! "$close_connection" && [[ $save == 0 ]] && \ [[ -n "$handshake_secret" ]] && [[ "$process_full" == all+ ]]; then - tls_version="$DETECTED_TLS_VERSION" + tls_version="$DETECTED_TLS_VERSION" if [[ "${TLS_SERVER_HELLO:8:3}" == 7F1 ]]; then tls_version="${TLS_SERVER_HELLO:8:4}" elif [[ "$TLS_SERVER_HELLO" =~ 002B00027F1[0-9A-F] ]]; then @@ -16264,7 +16264,7 @@ tls_sockets() { handshake_traffic_keys="$(derive-handshake-traffic-keys "$cipher" "$handshake_secret" "$initial_msg_transcript" "client")" read -r key iv finished_key <<< "$handshake_traffic_keys" - [[ -z master_secret ]] && master_secret="$(derive-master-secret "$cipher" "$handshake_secret")" + master_secret="$(derive-master-secret "$cipher" "$handshake_secret")" master_traffic_keys="$(derive-application-traffic-keys "$cipher" "$master_secret" "$msg_transcript" server)" read -r server_key server_iv server_finished_key <<< "$master_traffic_keys" if [[ "$cipher" == *SHA256 ]]; then From 8854d89be589f6260b7eab291a708bc78cd04b4f Mon Sep 17 00:00:00 2001 From: Riccardo Germenia Date: Mon, 2 Sep 2024 14:28:03 +0200 Subject: [PATCH 10/12] update CREDITS.md and CHANGELOG.md --- CHANGELOG.md | 1 + CREDITS.md | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3aed6cdb1..297355257 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ * Don't use external pwd command anymore * Doesn't hang anymore when there's no local resolver * Added --mtls feature to support client authentication +* Added support for signature_algorithms_cert extension ### Features implemented / improvements in 3.0 diff --git a/CREDITS.md b/CREDITS.md index 1290d9da0..cbccc772e 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -75,6 +75,10 @@ Full contribution, see git log. - lots of cleanups - Shellcheck static analysis +* Riccardo Germenia + - add support for signature_algorithms_cert extension + - add Certificate message to TLS sockets + * Laine Gholson - avahi/mDNS support - HTTP2/ALPN From 4436c4b96feb354f9f38552e7442fd5b9f38b057 Mon Sep 17 00:00:00 2001 From: Riccardo Germenia Date: Mon, 2 Sep 2024 14:44:37 +0200 Subject: [PATCH 11/12] add more details to CHANGELOG.md and CREDITS.md --- CHANGELOG.md | 3 ++- CREDITS.md | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 297355257..cf8835163 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,7 +46,8 @@ * Compatible to GNU grep 3.8 * Don't use external pwd command anymore * Doesn't hang anymore when there's no local resolver -* Added --mtls feature to support client authentication +* Added --mtls feature to support client authentication +* Added list of signature_algorithms used for client authentication * Added support for signature_algorithms_cert extension diff --git a/CREDITS.md b/CREDITS.md index cbccc772e..f1bfa1255 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -76,6 +76,7 @@ Full contribution, see git log. - Shellcheck static analysis * Riccardo Germenia + - add list of signature algorithms for client auth - add support for signature_algorithms_cert extension - add Certificate message to TLS sockets From 6a1eb473c285f062aea860fffffafedea66656d0 Mon Sep 17 00:00:00 2001 From: Riccardo Germenia Date: Mon, 7 Jul 2025 18:20:16 +0200 Subject: [PATCH 12/12] extract extensions from certificate request message too --- testssl.sh | 51 ++++++++++++++++++++++++++------------------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/testssl.sh b/testssl.sh index 10a287390..b013d45e5 100755 --- a/testssl.sh +++ b/testssl.sh @@ -7803,7 +7803,8 @@ extract_new_tls_extensions() { tls_extensions=$(grep -a 'TLS server extension ' "$1" | \ sed -e 's/TLS server extension //g' -e 's/\" (id=/\/#/g' \ -e 's/,.*$/,/g' -e 's/),$/\"/g' \ - -e 's/elliptic curves\/#10/supported_groups\/#10/g') + -e 's/elliptic curves\/#10/supported_groups\/#10/g' \ + -e 's/unknown\/#50/signature algorithms cert\/#50/g') tls_extensions=$(echo $tls_extensions) # into one line if [[ -n "$tls_extensions" ]]; then @@ -10359,30 +10360,27 @@ run_server_defaults() { i+=1 done <<< "$CLIENT_AUTH_CA_LIST" fi - if [[ "$CLIENT_AUTH_SIGALGS_LIST_13" = "$CLIENT_AUTH_SIGALGS_LIST_12" ]]; then + if [[ "$CLIENT_AUTH_SIGALGS_LIST_13" == "$CLIENT_AUTH_SIGALGS_LIST_12" ]]; then pr_bold " Offered Signature Algorithms " out_row_aligned "$CLIENT_AUTH_SIGALGS_LIST_13" else if [[ $CLIENT_AUTH_SIGALGS_LIST_13 != empty ]]; then pr_bold " Offered Signature Algorithms (TLS 1.3) " - out_row_aligned "$CLIENT_AUTH_SIGALGS_LIST_13" + outln "$(out_row_aligned_max_width "$CLIENT_AUTH_SIGALGS_LIST_13" " " $TERM_WIDTH)" fi if [[ $CLIENT_AUTH_SIGALGS_LIST_12 != empty ]]; then pr_bold " Offered Signature Algorithms (TLS 1.2) " - out_row_aligned "$CLIENT_AUTH_SIGALGS_LIST_12" + outln "$(out_row_aligned_max_width "$CLIENT_AUTH_SIGALGS_LIST_12" " " $TERM_WIDTH)" fi fi - jsonID="clientAuth_Signature_Algorithms_TLS13" - fileout "$jsonID" "INFO" "$CLIENT_AUTH_SIGALGS_LIST_13" - jsonID="clientAuth_Signature_Algorithms_TLS12" - fileout "$jsonID" "INFO" "$CLIENT_AUTH_SIGALGS_LIST_12" + fileout "clientAuth_Signature_Algorithms_TLS13" "INFO" "$CLIENT_AUTH_SIGALGS_LIST_13" + fileout "clientAuth_Signature_Algorithms_TLS12" "INFO" "$CLIENT_AUTH_SIGALGS_LIST_12" if [[ "$CLIENT_AUTH_SIGALGS_CERT_LIST_13" != empty ]]; then pr_bold " Offered Signature Algorithms for Certificates " - out_row_aligned "$CLIENT_AUTH_SIGALGS_CERT_LIST_13" + outln "$(out_row_aligned_max_width "$CLIENT_AUTH_SIGALGS_LIST_13" " " $TERM_WIDTH)" fi - jsonID="clientAuth_Signature_Algorithms_Cert_TLS13" - fileout "$jsonID" "INFO" "$CLIENT_AUTH_SIGALGS_CERT_LIST_13" + fileout "clientAuth_Signature_Algorithms_Cert_TLS13" "INFO" "$CLIENT_AUTH_SIGALGS_CERT_LIST_13" fi @@ -14588,6 +14586,7 @@ parse_tls_serverhello() { 002F) tls_extensions+="TLS server extension \"certificate authorities\" (id=47), len=$extension_len\n" ;; 0030) tls_extensions+="TLS server extension \"oid filters\" (id=48), len=$extension_len\n" ;; 0031) tls_extensions+="TLS server extension \"post handshake auth\" (id=49), len=$extension_len\n" ;; + 0032) tls_extensions+="TLS server extension \"signature algorithms cert\" (id=50), len=$extension_len\n" ;; 3374) tls_extensions+="TLS server extension \"next protocol\" (id=13172), len=$extension_len\n" if [[ "$process_full" =~ all ]]; then local -i protocol_len @@ -14839,7 +14838,7 @@ parse_tls_serverhello() { done fi # Now parse the CertificateRequest message. - if [[ $tls_certificate_request_ascii_len -ne 0 ]] && [[ "$process_full" =~ all ]]; then + if [[ $tls_certificate_request_ascii_len -ne 0 ]] then # The CertificateRequest message is only present in TLS 1.3 and TLS 1.2, it has a 2 char identifier and a 6 char length. if [[ $tls_certificate_request_ascii_len -lt 8 ]]; then debugme echo "Malformed CertificateRequest Handshake message in ServerHello." @@ -14848,7 +14847,7 @@ parse_tls_serverhello() { fi # The extract_calist can be used to extract the extensions' data from the CertificateRequest message. # The second parameter is the TLS version, if it is provided extract_calist does not try to get it. - extract_calist "$tls_certificate_request_ascii" "${DETECTED_TLS_VERSION:2:2}" + extract_calist "$tls_certificate_request_ascii" "${DETECTED_TLS_VERSION}" # Can not find a way to check if it is optional or required [[ "$CLIENT_AUTH" = none ]] && CLIENT_AUTH="optional" fi @@ -21680,10 +21679,10 @@ print_dn() { return 0 } -# Given the OpenSSL output of a response from a TLS server (with the -msg option) +# Given the OpenSSL output of a response from a TLS server (with the -msg option), or the tls_socket output, # in which the response includes a CertificateRequest message, update the CLIENT_AUTH_CA_LIST, # CLIENT_AUTH_SIGALGS_LIST and CLIENT_AUTH_SIGALGS_CERT_LIST variables with data from the message. -# The second_parameter is optional and should contain the two low bytes of the TLS version. +# The second_parameter is optional and should contain the full two bytes of the TLS version. # If the second parameter is provided the first parameter should contain the certreq in the following format: # - hex format # - no spaces or new lines characters @@ -21721,18 +21720,21 @@ extract_calist() { certreq="${certreq:8}" else certreq="$response" - fi + fi # Extract the extensions' information - if "$is_tls13" || [[ 0x"$tls_version" = "0x04" ]]; then + if "$is_tls13" || [[ 0x"$tls_version" == "0x0304" ]]; then # struct { # opaque certificate_request_context<0..2^8-1>; # Extension extensions<2..2^16-1>; # } CertificateRequest; - # Context len + + # Length of the certificate_request_context is given in the first two bytes. len=2*$(hex2dec "${certreq:0:2}") + # We can ignore the certificate_request_context as it is not used. certreq="${certreq:$((len+2))}" len=2*$(hex2dec "${certreq:0:4}") certreq="${certreq:4}" + extensions_text="" while true; do [[ -z "$certreq" ]] && break type=$(hex2dec "${certreq:0:4}") @@ -21746,19 +21748,25 @@ extract_calist() { # Since the structure of this extension only has one element in it, we can take everything # after the two bytes which contain the length of the element. sigalgs="${sigalgs:4:el_len}" + extensions_text+="TLS server extension \"signature algorithms\" (id=13), len=$len\n" elif [[ $type -eq 47 ]]; then # This is the certificate_authorities extension calist="${certreq:8:len}" el_len=2*$(hex2dec "${calist:0:4}") calist="${calist:4:el_len}" + extensions_text+="TLS server extension \"certificate authorities\" (id=47), len=$len\n" elif [[ $type -eq 50 ]]; then # This is the signature_algorithms_cert extension sigalgs_cert="${certreq:8:len}" el_len=2*$(hex2dec "${sigalgs_cert:0:4}") sigalgs_cert="${sigalgs_cert:4:el_len}" + extensions_text+="TLS server extension \"signature algorithms cert\" (id=50), len=$len\n" fi certreq="${certreq:$((len+8))}" done + printf "$extensions_text" > "$TEMPDIR/tls_extensions.txt" + extract_new_tls_extensions "$TEMPDIR/tls_extensions.txt" + pr_blue "TLS server extensions: $TLS_EXTENSIONS\n" sigalgs_string="$(sigalgs_converter "$sigalgs")" CLIENT_AUTH_SIGALGS_LIST_13="${sigalgs_string}" [[ -z "$sigalgs_string" ]] && CLIENT_AUTH_SIGALGS_LIST_13="empty" @@ -22231,13 +22239,6 @@ determine_optimal_proto() { fi actual_proto="$(get_protocol $TMPFILE)" tmpfile_handle ${FUNCNAME[0]}.txt - # if the actual_proto is TLSv1.2 and TLSv1.3 is available it probably means that the - # current openssl version does not support TLSv1.3 so tls_sockets must be used to - # obtain information about the certificate request - if [[ $actual_proto == TLSv1.2 ]] && [[ "$(has_server_protocol "tls1_3")" -eq 0 ]] && [[ "$CLIENT_AUTH" != none ]]; then - # this call performs the handshake and retrieves the signature_algorithms_cert information - tls_sockets "04" "$TLS13_CIPHER" "all+" "" "" false - fi return 0 }