🔗

TLS errors diagnostic

2021-11-19

So your .NET application is trying to call an https endpoint and fails with a message similar to the following?

System.Net.Http.HttpRequestException:
An error occurred while sending the request.
---> System.Net.WebException:
The request was aborted: Could not create SSL/TLS secure channel.

Or maybe like this one?

System.Net.Http.HttpRequestException:
An error occurred while sending the request.
---> System.Net.WebException:
The underlying connection was closed: Could not establish trust relationship for the SSL/TLS secure channel.
---> System.Security.Authentication.AuthenticationException:
The remote certificate is invalid according to the validation procedure.

Let's see what we can do...

Note: I will assume we are on Windows and using the native TLS implementation (Schannel)

Table of content

Possible causes

Among the most likely causes for these errors:

  1. Mismatched TLS version between client and server.
  2. No common cipher suite between client and server.
  3. The server certificate is considered as invalid by the client.
    • It might have expired (or its validity could be in the future, or the client and server clocks might be desynchronized)
    • It might not be signed by a certificate authority the client trusts (or the signature could be invalid)
    • It might not be valid for the DNS name we are trying to access.
    • It might have been revoked (though I don't think the revocation status is checked unless explicitly configured to do so by the client)

Handshake failure

The first two problems will trigger a TLS handshake failure (error code 40).
No more information will be available for the simple reason that TLS does not provide any: if the server cannot communicate with the client, it will send a TLS alert and terminate the connection.

Screenshot

The client only supports TLS 1.0, but the server does not.

Bad certificate

For the case where the connection fails because of an invalid certificate (error code 42), the TLS alert will mention it, but it will be encrypted.

You will need the decrypted conversation to see the actual error (or rely on information provided by your application).

Screenshot

The server certificate is invalid. The client sent back an encrypted alert.

Screenshot

If we have the shared secret and are able to decrypt TLS traffic, we can see the actual alert: "Bad Certificate".

Diagnosing with .NET network tracing

Note: The following is for a .NET Framework application. It's probably not directly applicable for a .NET Core application...

This method will get us all the information Schannel is able to report, without altering the system configuration or requiring admin rights.

It is very helpful to diagnose bad certificates, but will not be sufficient to diagnose the cause of a handshake failure (because TLS does not report the cause of such errors).

.NET Framework comes with built-in network tracing, but it's off by default.
You will need to edit your App.config to enable it. Report to the Microsoft documentation for details.

If the problem was caused by a bad certificate, the resulting log should give you enough information to diagnose the cause of the error.
You might also get a simple IllegalMessage error that could apply to multiple situations. In this case, you will have to make a network capture (see the next section about that).

Here is an example trace log where the server certificate was misconfigured (the DNS name in the certificate and the actual server host name were different):
(sensitive data removed)

System.Net Information: 0 : [33444] InitializeSecurityContext(In-Buffers count=2, Out-Buffer length=0, returned code=OK).
System.Net Information: 0 : [33444] Remote certificate: [Version]
  V3

[Subject]
  CN=...
  Simple Name: ...
  DNS Name: ...

[Issuer]
  CN=R3, O=Let's Encrypt, C=US
  Simple Name: R3
  DNS Name: R3

[Serial Number]
  ...

[Not Before]
  2021-11-08 ...

[Not After]
  2022-02-06 ...

[Thumbprint]
  ...

[Signature Algorithm]
  sha256RSA(1.2.840.113549.1.1.11)

[Public Key]
  Algorithm: RSA
  Length: 2048
  Key Blob: ...

System.Net Information: 0 : [33444] SecureChannel#41622463 - Remote certificate has errors:
System.Net Information: 0 : [33444] SecureChannel#41622463 - 	Certificate name mismatch.
System.Net Information: 0 : [33444] SecureChannel#41622463 - Remote certificate was verified as invalid by the user.

System.Net Information: 0 : [33444] ApplyControlToken() returned OK.
System.Net Information: 0 : [33444] InitializeSecurityContext(credential = System.Net.SafeFreeCredential_SECURITY, context = 51ed420:51efc08, targetName = [...], inFlags = ReplayDetect, SequenceDetect, Confidentiality, AllocateMemory, InitManualCredValidation)
System.Net Information: 0 : [33444] InitializeSecurityContext(In-Buffer length=0, Out-Buffer length=31, returned code=OK).

System.Net Error: 0 : [23832] Exception in HttpWebRequest#58870012:: - The underlying connection was closed: Could not establish trust relationship for the SSL/TLS secure channel..

Diagnosing with network captures

The .NET level tracing is sufficient for bad certificates, but diagnosing TLS handshake failures is harder: the connection is closed without details about the failure cause.
The only way I found for these cases is to capture actual network traffic and manually analyze it.

1. Capture traffic

The first step is to capture the relevant TLS packets.

To do this, use Wireshark/tshark (the CLI Wireshark).
Or use the built-in Windows capture tool (requires admin privileges, like Wireshark):

netsh trace start capture=yes report=disabled tracefile=trace.etl
netsh trace stop

I will not detail the procedure here. Just make sure you are capturing traffic on the correct interface (or for all interfaces if you are not sure)...

The netsh command outputs an ETL file that needs to be converted to a pcapng so we can analyze it with Wireshark/tshark.
Use this tool for conversion: github.com/microsoft/etl2pcapng

etl2pcapng.exe trace.etl trace.pcapng

2. Analyze TLS errors

It's possible to use Wireshark, but since I want to try and automate this process as much as possible, I will be using tshark with specially tailored filters.

The steps are similar and filters are the same for Wireshark.

Here is the official tshark manual: www.wireshark.org/docs/man-pages/tshark.html

tshark options

  • -2
    • Perform a two-pass analysis. This causes tshark to buffer output until the entire first pass is done, but allows it to fill in fields that require future knowledge, such as 'response in frame #' fields. Also permits reassembly frame dependencies to be calculated correctly.
  • -o "tcp.reassemble_out_of_order:true"
    • Often necessary for tshark to parse HTTP2/TLS streams properly (Without it, the certificate handshake is not decoded) You might need to set this option for Wireshark analysis too (in the TCP preferences, this is the "Reassemble out-of-order segments" checkbox)
  • -o "gui.column.format:..."
    • Only way I found to add the stream number in the output data without losing the default useful information.
      tshark option to display only some fields (-T fields and -e ...) cannot output human readable info...
  • -O tls
    • Output all information available about the decoded packets for the specified protocol (tls here)
  • -Y
    • Display filter. (specifying -Y instead of simply putting the filter at the end allows to put it anywhere in the command)
  • -r
    • Read a pcapng file as input.

Wireshark/tshark filters

  • tls.record.content_type eq 21
    • Only display TLS alert packets
  • tls.record.content_type in {21 22}
    • Display both TLS alerts and TLS handshake packets (which contain all the crypto negotiation and certificate/key exchanges)
  • tcp.stream eq 14
    • Filter for a specific TCP stream

Ready-made commands

The following commands can be used as-is (just replace mycapture.pcapng with your file)

Note the commands are escaped for the classic Windows CLI.
With Powershell, it's possible to use single quotes, so the escaping becomes something like the following:

./tshark --color -2 -o 'tcp.reassemble_out_of_order:true' -o 'gui.column.format:"Time","%Yt","Stream","%Cus:tcp.stream","Source","%s","Destination","%d","Protocol","%p","Info","%i"' -Y 'tls.record.content_type in {21 22} && tcp.stream eq 14' -r mycapture.pcapng

Display all the TLS alerts (content type 21) in the pcap.
The second column is the TCP stream number.

"C:\Program Files\Wireshark\tshark.exe" --color -2 -o "tcp.reassemble_out_of_order:true" -o "gui.column.format:\"Time\",\"%Yt\",\"Stream\",\"%Cus:tcp.stream\",\"Source\",\"%s\",\"Destination\",\"%d\",\"Protocol\",\"%p\",\"Info\",\"%i\"" -Y "tls.record.content_type eq 21" -r mycapture.pcapng

Screenshot

This capture contains multiple failures. The TCP stream number is the second column, after the date.

In this capture, the two TLS alerts happening at the same time are actually the same one, captured on two different interfaces, one physical, one from a virtual machine.

Let's analyze the first handshake failure.
The following will display TLS alerts and TLS handshake packets (content type 21 and 22) for stream 14.
(With Wireshark, you would instead right click the packet and select "Follow > TCP Stream".)

"C:\Program Files\Wireshark\tshark.exe" --color -2 -o "tcp.reassemble_out_of_order:true" -o "gui.column.format:\"Time\",\"%Yt\",\"Stream\",\"%Cus:tcp.stream\",\"Source\",\"%s\",\"Destination\",\"%d\",\"Protocol\",\"%p\",\"Info\",\"%i\"" -Y "tls.record.content_type in {21 22} && tcp.stream eq 14" -r mycapture.pcapng

Screenshot

Since it's a handshake failure, we only have a client hello before the server closed the connection.

Add -O tls to output the whole decoded packets for the TLS protocol, with the selected packets.
In Wireshark, you would instead display the details of theClient Hello packet.

"C:\Program Files\Wireshark\tshark.exe" --color -2 -o "tcp.reassemble_out_of_order:true" -o "gui.column.format:\"Time\",\"%Yt\",\"Stream\",\"%Cus:tcp.stream\",\"Source\",\"%s\",\"Destination\",\"%d\",\"Protocol\",\"%p\",\"Info\",\"%i\"" -Y "tls.record.content_type in {21 22} && tcp.stream eq 14" -r mycapture.pcapng -O tls

The result of this command will probably be a lot of text. You can add > out.log to output all the data to a new out.log file.

Here is the complete and unaltered output of the previous command:

Frame 1426: 228 bytes on wire (1824 bits), 228 bytes captured (1824 bits) on interface \Device\NPF_{465FCAB1-67D3-4DD1-BE13-F37A177C1187}, id 12
Ethernet II, Src: Microsof_01:3e:00 (00:15:5d:01:3e:00), Dst: Microsof_64:a0:ad (00:15:5d:64:a0:ad)
Internet Protocol Version 4, Src: 172.21.226.129, Dst: 185.245.136.237
Transmission Control Protocol, Src Port: 50159, Dst Port: 443, Seq: 1, Ack: 1, Len: 174
Transport Layer Security
    TLSv1.2 Record Layer: Handshake Protocol: Client Hello
        Content Type: Handshake (22)
        Version: TLS 1.2 (0x0303)
        Length: 169
        Handshake Protocol: Client Hello
            Handshake Type: Client Hello (1)
            Length: 165
            Version: TLS 1.2 (0x0303)
            Random: 6197c7e2272a02f0845d1d4d00b4ed06a65d500d35a6b3bd9bd39bddc03d1edc
                GMT Unix Time: Nov 19, 2021 16:50:58.000000000 Romance Standard Time
                Random Bytes: 272a02f0845d1d4d00b4ed06a65d500d35a6b3bd9bd39bddc03d1edc
            Session ID Length: 0
            Cipher Suites Length: 42
            Cipher Suites (21 suites)
                Cipher Suite: TLS_RSA_WITH_AES_128_CBC_SHA256 (0x003c)
                Cipher Suite: TLS_RSA_WITH_AES_128_CBC_SHA (0x002f)
                Cipher Suite: TLS_RSA_WITH_AES_256_CBC_SHA256 (0x003d)
                Cipher Suite: TLS_RSA_WITH_AES_256_CBC_SHA (0x0035)
                Cipher Suite: TLS_RSA_WITH_RC4_128_SHA (0x0005)
                Cipher Suite: TLS_RSA_WITH_3DES_EDE_CBC_SHA (0x000a)
                Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 (0xc027)
                Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (0xc013)
                Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (0xc014)
                Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (0xc02b)
                Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 (0xc023)
                Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 (0xc02c)
                Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384 (0xc024)
                Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA (0xc009)
                Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA (0xc00a)
                Cipher Suite: TLS_DHE_DSS_WITH_AES_128_CBC_SHA256 (0x0040)
                Cipher Suite: TLS_DHE_DSS_WITH_AES_128_CBC_SHA (0x0032)
                Cipher Suite: TLS_DHE_DSS_WITH_AES_256_CBC_SHA256 (0x006a)
                Cipher Suite: TLS_DHE_DSS_WITH_AES_256_CBC_SHA (0x0038)
                Cipher Suite: TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA (0x0013)
                Cipher Suite: TLS_RSA_WITH_RC4_128_MD5 (0x0004)
            Compression Methods Length: 1
            Compression Methods (1 method)
                Compression Method: null (0)
            Extensions Length: 82
            Extension: renegotiation_info (len=1)
                Type: renegotiation_info (65281)
                Length: 1
                Renegotiation Info extension
                    Renegotiation info extension length: 0
            Extension: server_name (len=28)
                Type: server_name (0)
                Length: 28
                Server Name Indication extension
                    Server Name list length: 26
                    Server Name Type: host_name (0)
                    Server Name length: 23
                    Server Name: snc18pp.snc.oodrive.com
            Extension: status_request (len=5)
                Type: status_request (5)
                Length: 5
                Certificate Status Type: OCSP (1)
                Responder ID list Length: 0
                Request Extensions Length: 0
            Extension: supported_groups (len=6)
                Type: supported_groups (10)
                Length: 6
                Supported Groups List Length: 4
                Supported Groups (2 groups)
                    Supported Group: secp256r1 (0x0017)
                    Supported Group: secp384r1 (0x0018)
            Extension: ec_point_formats (len=2)
                Type: ec_point_formats (11)
                Length: 2
                EC point formats Length: 1
                Elliptic curves point formats (1)
                    EC point format: uncompressed (0)
            Extension: signature_algorithms (len=16)
                Type: signature_algorithms (13)
                Length: 16
                Signature Hash Algorithms Length: 14
                Signature Hash Algorithms (7 algorithms)
                    Signature Algorithm: rsa_pkcs1_sha256 (0x0401)
                        Signature Hash Algorithm Hash: SHA256 (4)
                        Signature Hash Algorithm Signature: RSA (1)
                    Signature Algorithm: rsa_pkcs1_sha384 (0x0501)
                        Signature Hash Algorithm Hash: SHA384 (5)
                        Signature Hash Algorithm Signature: RSA (1)
                    Signature Algorithm: rsa_pkcs1_sha1 (0x0201)
                        Signature Hash Algorithm Hash: SHA1 (2)
                        Signature Hash Algorithm Signature: RSA (1)
                    Signature Algorithm: ecdsa_secp256r1_sha256 (0x0403)
                        Signature Hash Algorithm Hash: SHA256 (4)
                        Signature Hash Algorithm Signature: ECDSA (3)
                    Signature Algorithm: ecdsa_secp384r1_sha384 (0x0503)
                        Signature Hash Algorithm Hash: SHA384 (5)
                        Signature Hash Algorithm Signature: ECDSA (3)
                    Signature Algorithm: ecdsa_sha1 (0x0203)
                        Signature Hash Algorithm Hash: SHA1 (2)
                        Signature Hash Algorithm Signature: ECDSA (3)
                    Signature Algorithm: SHA1 DSA (0x0202)
                        Signature Hash Algorithm Hash: SHA1 (2)
                        Signature Hash Algorithm Signature: DSA (2)

Frame 1427: 61 bytes on wire (488 bits), 61 bytes captured (488 bits) on interface \Device\NPF_{465FCAB1-67D3-4DD1-BE13-F37A177C1187}, id 12
Ethernet II, Src: Microsof_64:a0:ad (00:15:5d:64:a0:ad), Dst: Microsof_01:3e:00 (00:15:5d:01:3e:00)
Internet Protocol Version 4, Src: 185.245.136.237, Dst: 172.21.226.129
Transmission Control Protocol, Src Port: 443, Dst Port: 50159, Seq: 1, Ack: 175, Len: 7
Transport Layer Security
    TLSv1.2 Record Layer: Alert (Level: Fatal, Description: Handshake Failure)
        Content Type: Alert (21)
        Version: TLS 1.2 (0x0303)
        Length: 2
        Alert Message
            Level: Fatal (2)
            Description: Handshake Failure (40)

Manual analysis

Ok, so we have the complete information our client application sent to the server, just before the server closed the connection.

What do we do with this?

We only have information about the client, but what about the server?
You might know the TLS server configuration, but let's make sure.

SSL Labs

SSL Labs will analyse the server and report what TLS versions and cipher suites it supports.

Screenshot

TLS versions our server supports, as reported by SSL Labs.

In our case, we see that the server only supports TLS 1.2.
Since our client sent a TLS 1.2 client hello, the problem is elsewhere.

SSL Labs also reports the cipher suites supported:

  • TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (0xc030)
  • TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 (0xcca8)
  • TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (0xc02f)

And is friendly enough to simulate connections with various clients.
Among them we can see several failures.

Screenshot

Excerpt of the connections with simulated devices, from SSL Labs.

This is where the dots connect: it turns out our client app is running on Windows 7, and none of the Schannel cipher suites enabled on this OS are compatible with the cipher suites advertised by the server!

The sever was hardened to support only strong cipher suites (ciphersuite.info is a good source), and inadvertently dropped Windows 7 compatibility in the process.

Nmap

SSL Labs is more friendly, but nmap can also be used to do the same thing, and it's faster:

$ nmap --script ssl-enum-ciphers -p 443 snc18pp.snc.oodrive.com
Starting Nmap 7.80 ( https://nmap.org ) at 2021-11-19 17:06 CET
Nmap scan report for snc18pp.snc.oodrive.com (185.245.136.237)
Host is up (0.0031s latency).

PORT    STATE SERVICE
443/tcp open  https
| ssl-enum-ciphers:
|   TLSv1.2:
|     ciphers:
|       TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (secp384r1) - A
|       TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 (secp384r1) - A
|       TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (secp384r1) - A
|     compressors:
|       NULL
|     cipher preference: server
|_  least strength: A

Nmap done: 1 IP address (1 host up) scanned in 1.68 seconds

You will then have to manually try to match a cipher suite between the server and the client...

Curl

If you want to do some quick testing with another client application, curl can be helpful.

Be careful though: on Windows, depending on how it's compiled, curl might use openssl instead of Schannel.
It means that curl might succeed while applications using the OS TLS stack will not.

Some versions of curl are compiled to support mutiple TLS implementations.

In this case, you can switch between them by setting an environment variable.

For example to use Schannel:

set CURL_SSL_BACKEND=schannel
curl -v https://snc18pp.snc.oodrive.com

To use a specific TLS version, use --tls-max 1.0 (the previous works with Schannel, while --tlsv1.0 only works with openssl)

With some TLS implementations, you can restrict which cipher suites to use with --ciphers. More info here: curl.se/docs/ssl-ciphers.html

Conclusion

I hope this was helpful.

Since capturing network packets requires admin privileges, specific tools knowledge, and might be a non-starter on production servers, I looked for a way to diagnose TLS handshake failures without resorting to them, but beside guessing, it still seems this is the only method...

There is another way to log Schannel events I didn't talk about, because I didn't find it very useful compared to the two methods I documented above.
If you are not able to add traces to your app though, it might interest you: kevinjustin.com/blog/2017/11/08/schannel-event-logging

If you have another solution, please share!

0 comment



Formatting cheat sheet.
The current page url links to a specific comment.
The comment is shown highlighted below in context.

    JavaScript is required to see the comments. Sorry...