Search code examples
c#.netdotnet-httpclient

HttpClient with client certificate misbehaves on linux


I set up a request as follows:

var url = "https://api.the.url.com/path/ver/endpoint";
var certFile = "TheFile.pfx";
var certPass = "ThePassword";

var cert = new X509Certificate2(
    Path.Combine(AppContext.BaseDirectory, certFile),
    certPass, X509KeyStorageFlags.MachineKeySet);

var handler = new SocketsHttpHandler();
handler.SslOptions.ClientCertificates = new X509CertificateCollection();
handler.SslOptions.ClientCertificates.Add(cert);
handler.SslOptions.LocalCertificateSelectionCallback = (_,_,_,_,_) => cert;

var httpClient = HttpClientFactory.Create(handler);
var result = await httpClient.GetAsync(url);
result.EnsureSuccessStatusCode();

This occasionally fails on Windows, but on Linux it always fails:

The SSL connection could not be established, see inner exception.
Unable to write data to the transport connection: Connection reset by peer.
Connection reset by peer

When I looked at the tcp traffic, the handshake stops when (me) the client ACKs the server's Server Key Exchange/Server Hello Done:

Disclaimer: These are windows traffic dump, I am still working on getting these on Linux

TCP        66    44222 → 443 [SYN, ECE, CWR] Seq=0 Win=64240 Len=0 MSS=1460 WS=256 SACK_PERM
TCP        66    443 → 44222 [SYN, ACK, ECE] Seq=0 Ack=1 Win=14600 Len=0 MSS=1436 WS=1 SACK_PERM
TCP        54    44222 → 443 [ACK] Seq=1 Ack=1 Win=262656 Len=0
TLSv1.2   328    Client Hello
TCP        60    443 → 44222 [ACK] Seq=1 Ack=275 Win=14874 Len=0
TLSv1.2  1514    Server Hello
TCP      1514    443 → 44222 [ACK] Seq=1461 Ack=275 Win=14874 Len=1460 [TCP segment of a reassembled PDU]
TCP        54    44222 → 443 [ACK] Seq=275 Ack=2921 Win=262656 Len=0
TLSv1.2  1514    Certificate, Server Key Exchange
TLSv1.2   360    Certificate Request, Server Hello Done
TCP        54    44222 → 443 [ACK] Seq=275 Ack=4687 Win=262656 Len=0
TCP       109    443 → 44222 [RST, ACK] Seq=4687 Ack=275 Win=0 Len=55

Instead of RST, ACK I am supposed to get something like this:

TLSv1.2   2043    Certificate, Client Key Exchange, Certificate Verify, Change Cipher Spec, Encrypted Handshake Message
TCP         60    443 → 44566 [ACK] Seq=4687 Ack=2264 Win=16863 Len=0
TLSv1.2    105    Change Cipher Spec, Encrypted Handshake Message
TLSv1.2    153    Application Data
TCP         60    443 → 44566 [ACK] Seq=4738 Ack=2363 Win=16962 Len=0
TLSv1.2    642    Application Data
TLSv1.2     88    Application Data
TCP         54    44566 → 443 [ACK] Seq=2363 Ack=5360 Win=261888 Len=0
TCP         54    44566 → 443 [RST, ACK] Seq=2363 Ack=5360 Win=0 Len=0

This code used to work for some time, and for some unknown reason stopped working. Running the equivalent cUrl request seems to work as expected on the Linux machine. So it appears to be .NET related. I tried to use RestSharp but I think under the hood it is using HttpClient and suffered the same error. I ran nmap --script ssl-enum-ciphers against the target url, and I changed the http handler to use the mentioned ssl protocol and cipher suites, but that also did not change the outcome.

What should I try next?

Update 1:

I managed to pull the tcp dump from the Linux system, and the RST was caused by ssl handshake timeout exceeded. I don't think we can skip the ssl verification using HttpClient, only ignore its outcome. I tried with SslStream which also does not have any such setting.

Update 2:

I added event listener and printing them in the console. Also, I ran the code in a linux vm so wireshark can sniff ALL traffic through the virtual interface (previously I was filtering for the target ip only).


Solution

  • I added an event listener around the HttpClient; it prints events from System.Net and System.Security. I found out that after the server sends the Server Certificate/Server Hello Done, System.Security.Cryptography.X509Certificates.X509Chain.OpenSsl is attempting to fetch issuing certificate defined in the client certificate. The end point for that can't be reached causing the handshake timeout. The best solution was to replace the client certificate used to make the request. An alternative solution is to block this url in your firewall, this will cause the request to fail right away avoiding timeout.