mirror of
https://github.com/drwetter/testssl.sh.git
synced 2025-01-03 23:39:45 +01:00
Derive handshake traffic key for TLSv1.3
This is the first in a series of PRs to add support for processing the encrypted portions of the server's response in a TLSv1.3 handshake. This PR adds the code to derive the handshake traffic key needed to decrypt the response (the next PR will add the code to perform the symmetric-key decryption of the encrypted portions of the response). Since this PR does not make use of the traffic key that it derives, it doesn't yet add any new functionality. Note that testssl.sh will not always be able to derive the session keys. If the version of OpenSSL that is bundled with testssl.sh is used and the server chooses to use an X25519 ephemeral key, OpenSSL will be unable to perform the shared secret in derive-handshake-traffic-secret(). (OpenSSL 1.1.0 supports X25519.) Since X25519 use a different encoding than ECDH keys, the lack of X25519 support will be discovered in parse_tls_serverhello() when $OPENSSL pkey is unable to convert the key from DER to PEM. So, in debugging mode, parse_tls_serverhello() now displays a warning if it receives a key share that $OPENSSL pkey cannot handle.
This commit is contained in:
parent
1984d7fc90
commit
cfecc3c8c4
280
testssl.sh
280
testssl.sh
@ -8491,6 +8491,257 @@ parse_sslv2_serverhello() {
|
||||
return $ret
|
||||
}
|
||||
|
||||
# arg1: hash function
|
||||
# arg2: key
|
||||
# arg3: text
|
||||
hmac() {
|
||||
local hash_fn="$1"
|
||||
local key="$2" text="$3" output
|
||||
local -i ret
|
||||
|
||||
output="$(asciihex_to_binary_file "$text" "/dev/stdout" | $OPENSSL dgst "$hash_fn" -mac HMAC -macopt hexkey:"$key" 2>/dev/null)"
|
||||
ret=$?
|
||||
tm_out "$(awk '/=/ { print $2 }' <<< "$output")"
|
||||
return $ret
|
||||
}
|
||||
|
||||
# arg1: hash function
|
||||
# arg2: pseudorandom key (PRK)
|
||||
# arg2: info
|
||||
# arg3: length of output keying material in octets
|
||||
# See RFC 5869, Section 2.3
|
||||
hkdf-expand() {
|
||||
local hash_fn="$1"
|
||||
local prk="$2" info="$3" output=""
|
||||
local -i out_len="$4"
|
||||
local -i i n mod_check hash_len ret
|
||||
local counter
|
||||
local ti tim1 # T(i) and T(i-1)
|
||||
|
||||
case "$hash_fn" in
|
||||
"-sha256") hash_len=32 ;;
|
||||
"-sha384") hash_len=48 ;;
|
||||
*) return 7
|
||||
esac
|
||||
|
||||
n=$out_len/$hash_len
|
||||
mod_check=$out_len%$hash_len
|
||||
[[ $mod_check -ne 0 ]] && n+=1
|
||||
|
||||
tim1=""
|
||||
for (( i=1; i <= n; i++ )); do
|
||||
counter="$(printf "%02X\n" $i)"
|
||||
ti="$(hmac "$hash_fn" "$prk" "$tim1$info$counter")"
|
||||
[[ $? -ne 0 ]] && return 7
|
||||
output+="$ti"
|
||||
tim1="$ti"
|
||||
done
|
||||
out_len=2*$out_len
|
||||
tm_out "${output:0:out_len}"
|
||||
return 0
|
||||
}
|
||||
|
||||
# arg1: hash function
|
||||
# arg2: secret
|
||||
# arg3: label
|
||||
# arg4: context
|
||||
# arg5: length
|
||||
# See draft-ietf-tls-tls13, Section 7.1
|
||||
hkdf-expand-label() {
|
||||
local hash_fn="$1"
|
||||
local secret="$2" label="$3"
|
||||
local context="$4"
|
||||
local -i length="$5"
|
||||
local hkdflabel hkdflabel_label hkdflabel_context
|
||||
local hkdflabel_length
|
||||
local -i len
|
||||
|
||||
hkdflabel_length="$(printf "%04X\n" $length)"
|
||||
if [[ "${TLS_SERVER_HELLO:8:2}" == "7F" ]] && [[ 0x${TLS_SERVER_HELLO:10:2} -lt 0x14 ]]; then
|
||||
# "544c5320312e332c20" = "TLS 1.3, "
|
||||
hkdflabel_label="544c5320312e332c20$label"
|
||||
else
|
||||
# "746c73313320" = "tls13 "
|
||||
hkdflabel_label="746c73313320$label"
|
||||
fi
|
||||
len=${#hkdflabel_label}/2
|
||||
hkdflabel_label="$(printf "%02X\n" $len)$hkdflabel_label"
|
||||
len=${#context}/2
|
||||
hkdflabel_context="$(printf "%02X\n" $len)$context"
|
||||
hkdflabel="$hkdflabel_length$hkdflabel_label$hkdflabel_context"
|
||||
|
||||
hkdf-expand "$hash_fn" "$secret" "$hkdflabel" "$length"
|
||||
return $?
|
||||
}
|
||||
|
||||
# arg1: hash function
|
||||
# arg2: secret
|
||||
# arg3: label
|
||||
# arg4: ASCII-HEX of messages
|
||||
# See draft-ietf-tls-tls13, Section 7.1
|
||||
derive-secret() {
|
||||
local hash_fn="$1"
|
||||
local secret="$2" label="$3" messages="$4"
|
||||
local hash_messages
|
||||
local -i hash_len retcode
|
||||
|
||||
case "$hash_fn" in
|
||||
"-sha256") hash_len=32 ;;
|
||||
"-sha384") hash_len=48 ;;
|
||||
*) return 7
|
||||
esac
|
||||
|
||||
hash_messages="$(asciihex_to_binary_file "$messages" "/dev/stdout" | $OPENSSL dgst "$hash_fn" 2>/dev/null | awk '/=/ { print $2 }')"
|
||||
hkdf-expand-label "$hash_fn" "$secret" "$label" "$hash_messages" "$hash_len"
|
||||
return $?
|
||||
}
|
||||
|
||||
# arg1: hash function
|
||||
# arg2: private key file
|
||||
# arg3: file containing server's ephemeral public key
|
||||
# arg4: ASCII-HEX of messages (ClientHello...ServerHello)
|
||||
# See key derivation schedule diagram in Section 7.1 of draft-ietf-tls-tls13
|
||||
derive-handshake-traffic-secret() {
|
||||
local hash_fn="$1"
|
||||
local priv_file="$2" pub_file="$3"
|
||||
local messages="$4"
|
||||
local -i i ret
|
||||
local early_secret derived_secret shared_secret handshake_secret
|
||||
|
||||
"$HAS_PKUTIL" || return 1
|
||||
|
||||
# early_secret="$(hmac "$hash_fn" "000...000" "000...000")"
|
||||
case "$hash_fn" in
|
||||
"-sha256") early_secret="33ad0a1c607ec03b09e6cd9893680ce210adf300aa1f2660e1b22e10f170f92a"
|
||||
if [[ "${TLS_SERVER_HELLO:8:2}" == "7F" ]] && [[ 0x${TLS_SERVER_HELLO:10:2} -lt 0x14 ]]; then
|
||||
# "6465726976656420736563726574" = "derived secret"
|
||||
# derived_secret="$(derive-secret "$hash_fn" "$early_secret" "6465726976656420736563726574" "")"
|
||||
derived_secret="c1c0c36bf8fb1d1afa949fbd360e71af69a6244a4c2eaef5bbbb6442a7277d2c"
|
||||
else
|
||||
# "64657269766564" = "derived"
|
||||
# derived_secret="$(derive-secret "$hash_fn" "$early_secret" "64657269766564" "")"
|
||||
derived_secret="6f2615a108c702c5678f54fc9dbab69716c076189c48250cebeac3576c3611ba"
|
||||
fi
|
||||
;;
|
||||
"-sha384") early_secret="7ee8206f5570023e6dc7519eb1073bc4e791ad37b5c382aa10ba18e2357e716971f9362f2c2fe2a76bfd78dfec4ea9b5"
|
||||
if [[ "${TLS_SERVER_HELLO:8:2}" == "7F" ]] && [[ 0x${TLS_SERVER_HELLO:10:2} -lt 0x14 ]]; then
|
||||
# "6465726976656420736563726574" = "derived secret"
|
||||
# derived_secret="$(derive-secret "$hash_fn" "$early_secret" "6465726976656420736563726574" "")"
|
||||
derived_secret="54c80fa05ee9e0532ce3db8ddeca37a0365683bcd3b27bdc88d2b9fdc115ca4ebc8edc1f0b72a6a0861e803fc34761ef"
|
||||
else
|
||||
# "64657269766564" = "derived"
|
||||
# derived_secret="$(derive-secret "$hash_fn" "$early_secret" "64657269766564" "")"
|
||||
derived_secret="1591dac5cbbf0330a4a84de9c753330e92d01f0a88214b4464972fd668049e93e52f2b16fad922fdc0584478428f282b"
|
||||
fi
|
||||
;;
|
||||
*) return 7
|
||||
esac
|
||||
|
||||
shared_secret="$($OPENSSL pkeyutl -derive -inkey "$priv_file" -peerkey "$pub_file" 2>/dev/null | hexdump -v -e '16/1 "%02X"')"
|
||||
|
||||
# For draft 18 use $early_secret rather than $derived_secret.
|
||||
if [[ "${TLS_SERVER_HELLO:8:4}" == "7F12" ]]; then
|
||||
handshake_secret="$(hmac "$hash_fn" "$early_secret" "${shared_secret%%[!0-9A-F]*}")"
|
||||
else
|
||||
handshake_secret="$(hmac "$hash_fn" "$derived_secret" "${shared_secret%%[!0-9A-F]*}")"
|
||||
fi
|
||||
[[ $? -ne 0 ]] && return 7
|
||||
|
||||
if [[ "${TLS_SERVER_HELLO:8:2}" == "7F" ]] && [[ 0x${TLS_SERVER_HELLO:10:2} -lt 0x14 ]]; then
|
||||
# "7365727665722068616e647368616b65207472616666696320736563726574" = "server handshake traffic secret"
|
||||
derived_secret="$(derive-secret "$hash_fn" "$handshake_secret" "7365727665722068616e647368616b65207472616666696320736563726574" "$messages")"
|
||||
else
|
||||
# "732068732074726166666963" = "s hs traffic"
|
||||
derived_secret="$(derive-secret "$hash_fn" "$handshake_secret" "732068732074726166666963" "$messages")"
|
||||
fi
|
||||
[[ $? -ne 0 ]] && return 7
|
||||
tm_out "$derived_secret"
|
||||
return 0
|
||||
}
|
||||
|
||||
# arg1: hash function
|
||||
# arg2: secret (created by derive-handshake-traffic-secret)
|
||||
# arg3: purpose ("key" or "iv")
|
||||
# arg4: length of the key
|
||||
# See draft-ietf-tls-tls13, Section 7.3
|
||||
derive-traffic-key() {
|
||||
local hash_fn="$1"
|
||||
local secret="$2" purpose="$3"
|
||||
local -i key_length="$4"
|
||||
local key
|
||||
|
||||
key="$(hkdf-expand-label "$hash_fn" "$secret" "$purpose" "" "$key_length")"
|
||||
[[ $? -ne 0 ]] && return 7
|
||||
tm_out "$key"
|
||||
return 0
|
||||
}
|
||||
|
||||
#arg1: TLS cipher
|
||||
#arg2: file containing cipher name, public key, and private key
|
||||
#arg3: First ClientHello, if response was a HelloRetryRequest
|
||||
#arg4: HelloRetryRequest, if one was sent
|
||||
#arg5: Final (or only) ClientHello
|
||||
#arg6: ServerHello
|
||||
derive-handshake-traffic-keys() {
|
||||
local cipher="$1"
|
||||
local tmpfile="$2"
|
||||
local clienthello1="$3" hrr="$4" clienthello2="$5" serverhello="$6"
|
||||
local hash_clienthello1
|
||||
local -i key_len
|
||||
local -i retcode
|
||||
local hash_fn
|
||||
local pub_file priv_file tmpfile
|
||||
|
||||
if [[ "$cipher" == *SHA256 ]]; then
|
||||
hash_fn="-sha256"
|
||||
elif [[ "$cipher" == *SHA384 ]]; then
|
||||
hash_fn="-sha384"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
if [[ "$cipher" == *AES_128* ]]; then
|
||||
key_len=16
|
||||
elif ( [[ "$cipher" == *AES_256* ]] || [[ "$cipher" == *CHACHA20_POLY1305* ]] ); then
|
||||
key_len=32
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
pub_file="$(mktemp "$TEMPDIR/pubkey.XXXXXX")" || return 7
|
||||
awk '/-----BEGIN PUBLIC KEY/,/-----END PUBLIC KEY/ { print $0 }' \
|
||||
"$tmpfile" > "$pub_file"
|
||||
[[ ! -s "$pub_file" ]] && return 1
|
||||
|
||||
priv_file="$(mktemp "$TEMPDIR/privkey.XXXXXX")" || return 7
|
||||
if grep -q "\-\-\-\-\-BEGIN EC PARAMETERS" "$tmpfile"; then
|
||||
awk '/-----BEGIN EC PARAMETERS/,/-----END EC PRIVATE KEY/ { print $0 }' \
|
||||
"$tmpfile" > "$priv_file"
|
||||
else
|
||||
awk '/-----BEGIN PRIVATE KEY/,/-----END PRIVATE KEY/ { print $0 }' \
|
||||
"$tmpfile" > "$priv_file"
|
||||
fi
|
||||
[[ ! -s "$priv_file" ]] && return 1
|
||||
|
||||
if [[ -n "$hrr" ]] && [[ "${serverhello:8:4}" == "7F12" ]]; then
|
||||
derived_secret="$(derive-handshake-traffic-secret "$hash_fn" "$priv_file" "$pub_file" "$clienthello1$hrr$clienthello2$serverhello")"
|
||||
elif [[ -n "$hrr" ]]; then
|
||||
hash_clienthello1="$(asciihex_to_binary_file "$clienthello1" "/dev/stdout" | $OPENSSL dgst "$hash_fn" 2>/dev/null | awk '/=/ { print $2 }')"
|
||||
derived_secret="$(derive-handshake-traffic-secret "$hash_fn" "$priv_file" "$pub_file" "FE0000$(printf "%02x" $((${#hash_clienthello1}/2)))$hash_clienthello1$hrr$clienthello2$serverhello")"
|
||||
else
|
||||
derived_secret="$(derive-handshake-traffic-secret "$hash_fn" "$priv_file" "$pub_file" "$clienthello2$serverhello")"
|
||||
fi
|
||||
retcode=$?
|
||||
rm $pub_file $priv_file
|
||||
[[ $retcode -ne 0 ]] && return 1
|
||||
# "6b6579" = "key"
|
||||
server_write_key="$(derive-traffic-key "$hash_fn" "$derived_secret" "6b6579" "$key_len")"
|
||||
[[ $? -ne 0 ]] && return 1
|
||||
# "6976" = "iv"
|
||||
server_write_iv="$(derive-traffic-key "$hash_fn" "$derived_secret" "6976" "12")"
|
||||
[[ $? -ne 0 ]] && return 1
|
||||
tm_out "$server_write_key $server_write_iv"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Return:
|
||||
# 0 if arg1 contains the entire server response.
|
||||
# 1 if arg1 does not contain the entire server response.
|
||||
@ -9142,6 +9393,13 @@ parse_tls_serverhello() {
|
||||
fi
|
||||
if [[ -n "$key_bitstring" ]]; then
|
||||
key_bitstring="$(asciihex_to_binary_file "$key_bitstring" "/dev/stdout" | $OPENSSL pkey -pubin -inform DER 2>$ERRFILE)"
|
||||
if [[ -z "$key_bitstring" ]] && [[ $DEBUG -ge 2 ]]; then
|
||||
if [[ -n "$named_curve_str" ]]; then
|
||||
prln_warning "Your $OPENSSL doesn't support $named_curve_str"
|
||||
else
|
||||
prln_warning "Your $OPENSSL doesn't support named curve $named_curve"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
@ -9585,6 +9843,8 @@ parse_tls_serverhello() {
|
||||
fi
|
||||
fi
|
||||
tmpfile_handle $FUNCNAME.txt
|
||||
|
||||
TLS_SERVER_HELLO="02$(printf "%06x" $(($tls_serverhello_ascii_len/2)))${tls_serverhello_ascii}"
|
||||
return 0
|
||||
}
|
||||
|
||||
@ -10090,7 +10350,7 @@ socksend_tls_clienthello() {
|
||||
printf -- "$data" >&5 2>/dev/null &
|
||||
sleep $USLEEP_SND
|
||||
|
||||
if [[ "$tls_low_byte" -gt 0x03 ]]; then
|
||||
if [[ "$tls_low_byte" -gt 0x03 ]]; then
|
||||
TLS_CLIENT_HELLO="$(tolower "$NW_STR")"
|
||||
TLS_CLIENT_HELLO="${TLS_CLIENT_HELLO//\\x0\\/\\x00\\}"
|
||||
TLS_CLIENT_HELLO="${TLS_CLIENT_HELLO//\\x1\\/\\x01\\}"
|
||||
@ -10338,9 +10598,11 @@ tls_sockets() {
|
||||
local cipher_list_2send
|
||||
local sock_reply_file2 sock_reply_file3
|
||||
local tls_hello_ascii next_packet
|
||||
local clienthello1 hrr=""
|
||||
local process_full="$3" offer_compression=false skip=false
|
||||
local close_connection=true
|
||||
local -i hello_done=0
|
||||
local cipher="" key_and_iv=""
|
||||
|
||||
[[ "$5" == "true" ]] && offer_compression=true
|
||||
[[ "$6" == "false" ]] && close_connection=false
|
||||
@ -10363,6 +10625,7 @@ tls_sockets() {
|
||||
|
||||
# if sending didn't succeed we don't bother
|
||||
if [[ $ret -eq 0 ]]; then
|
||||
clienthello1="$TLS_CLIENT_HELLO"
|
||||
sockread_serverhello 32768
|
||||
"$TLS_DIFFTIME_SET" && TLS_NOW=$(LC_ALL=C date "+%s")
|
||||
|
||||
@ -10373,6 +10636,7 @@ tls_sockets() {
|
||||
resend_if_hello_retry_request "$tls_hello_ascii" "$cipher_list_2send" "$4" "$process_full"
|
||||
ret=$?
|
||||
if [[ $ret -eq 2 ]]; then
|
||||
hrr="${tls_hello_ascii:10}"
|
||||
tls_hello_ascii=$(hexdump -v -e '16/1 "%02X"' "$SOCK_REPLY_FILE")
|
||||
tls_hello_ascii="${tls_hello_ascii%%[!0-9A-F]*}"
|
||||
elif [[ $ret -eq 1 ]] || [[ $ret -eq 6 ]]; then
|
||||
@ -10429,6 +10693,20 @@ tls_sockets() {
|
||||
check_tls_serverhellodone "$tls_hello_ascii" "$process_full"
|
||||
hello_done=$?
|
||||
if [[ "$hello_done" -eq 3 ]]; then
|
||||
debugme echo "reading server hello..."
|
||||
parse_tls_serverhello "$tls_hello_ascii" "ephemeralkey"
|
||||
ret=$?
|
||||
if [[ "$ret" -eq 0 ]] || [[ "$ret" -eq 2 ]]; then
|
||||
cipher=$(get_cipher "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt")
|
||||
if [[ -n "$hrr" ]]; then
|
||||
key_and_iv="$(derive-handshake-traffic-keys "$cipher" "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt" "$clienthello1" "$hrr" "$TLS_CLIENT_HELLO" "$TLS_SERVER_HELLO")"
|
||||
else
|
||||
key_and_iv="$(derive-handshake-traffic-keys "$cipher" "$TEMPDIR/$NODEIP.parse_tls_serverhello.txt" "" "" "$TLS_CLIENT_HELLO" "$TLS_SERVER_HELLO")"
|
||||
fi
|
||||
[[ $? -ne 0 ]] && hello_done=2
|
||||
else
|
||||
hello_done=2
|
||||
fi
|
||||
# The following three lines are temporary until the code
|
||||
# to decrypt TLSv1.3 responses has been added, at which point
|
||||
# parse_tls_serverhello() will be called with process_full="all"
|
||||
|
Loading…
Reference in New Issue
Block a user