Runtime: Linux - SslStream.AuthenticateAsClient is not presenting client certificates to server

Created on 25 Jan 2016  路  15Comments  路  Source: dotnet/runtime

_This issue exists on Linux only_

When establishing a connection to a TCP server where a client certificate is required, the client certificate does not get passed to the server side. The connection then gets aborted by the server, resulting in an exception as below.

Using NetMon, we can observe during that handshake that the server successfully sends its certificate to the client, but the client never presents the client certificate to the server. At this point, the server will send an RST and the connection is killed.

I've isolated the repro to using just a TcpClient and TcpServer + SslStream - see https://gist.github.com/iamjasonp/27cd1ebce287f8b51d0e for the rudimentary repro.

Platforms tested

  • .NET Desktop - :white_check_mark:
  • .NET Core - Windows: :white_check_mark:
  • .NET Core - Linux: :x:

First chance exception caught:

'System.Security.Authentication.AuthenticationException' in System.dll
Additional information: The remote certificate is invalid according to the validation procedure.

Server call stack:

System.dll!System.Net.Security.SslState.ForceAuthentication(bool receiveFirst, byte[] buffer, System.Net.AsyncProtocolRequest asyncRequest)
System.dll!System.Net.Security.SslState.ProcessAuthentication(System.Net.LazyAsyncResult lazyResult)
System.dll!System.Net.Security.SslStream.AuthenticateAsServer(System.Security.Cryptography.X509Certificates.X509Certificate serverCertificate, bool clientCertificateRequired, System.Security.Authentication.SslProtocols enabledSslProtocols, bool checkCertificateRevocation)
TcpRepro-Server.exe!Examples.System.Net.SslTcpServer.ProcessClient(System.Net.Sockets.TcpClient client) 
TcpRepro-Server.exe!Examples.System.Net.SslTcpServer.RunServer() 
TcpRepro-Server.exe!Examples.System.Net.SslTcpServer.Main(string[] args)

This also looks to be the root cause of the issue in https://github.com/dotnet/wcf/issues/619#issuecomment-173177433

area-System.Net os-linux

Most helpful comment

@realityexists What is the value of clientCert.HasPrivateKey? If it's false then there's logic to search the CurrentUser\My and (on non-Linux) LocalMachine\My stores to find a matching cert which has a private key; which you may have installed in Windows but not Linux. If it's true (e.g. the cert file is a PFX with a private key) then it should work since the private key is already known.

All 15 comments

Which version of the packages are you using? This should have been fixed with https://github.com/dotnet/corefx/pull/5551

We were using

  "dependencies": {
    "System.Net.Primitives": "4.0.11-rc2-23712",
    "System.Net.Sockets": "4.0.1-rc2-23712",
    "System.Net.Security": "4.0.0-rc2-23712",

I'll double check with newer packages and see what happens

Thanks, @iamjasonp. I'm going to close this on the assumption that it is the problem. If the issue persists after updating to today's packages, please re-open.

Hey @stephentoub - was trying this with packages from rc3-23725, and unfortunately the issue is still happening for me on Linux.

On Windows (.NET Desktop and .NET Core), this scenario continues to work.

cc @mconnew; I think what you're doing might be relevant to the conversation

Looks like there were a couple different problems:
1) Due to underlying API differences between SChannel and OpenSSL the Unix builds never got into a state where they thought they were supposed to bind the client cert handles.
2) The server side didn't like receiving certificates with error-containing X509Chains. The chain got built twice, the first time decided if we aborted the handshake; the second was the one that fed into the user callback.
3) The test that would have told us all this was disabled due to recurring intermittent timeouts on Windows
4) Previously the tests were only run on Windows anyways.

Playing around with some more things and ensuring that there's adequate test coverage. I expect to issue a PR today.

@bartonjs :
Not sure if this is related to this specific issue, but it does have some similarity with the description.
On Windows 7:
stream.AuthenticateAsClientAsync(host, certificates, SslProtocols.Tls, true) properly authenticates the client with the server and moves on to send/read.
On Ubuntu 16.04 same call kills Kestrel process with no exception.
systemd[1]: kestrel-xxx.service: Main process exited, code=killed, status=9/KILL
systemd[1]: kestrel-xxx.service: Unit entered failed state.
systemd[1]: kestrel-xxx.service: Failed with result 'signal'.

Windows 7: dotnet --version 1.0.0-preview3-004056
Ubuntu 16.04: dotnet --version: 1.0.0-preview2-1-003177

I'm running into this issue again under Ubuntu 16.04. dotnet --version = 2.0.0

I'm trying to connect to a Dovecot IMAP server configured to require client SSL certificates. Dovecot logs show that no certificate is sent when the client is running on Linux, but it is sent (and authentication succeeds) when running on Windows 7.

Minimal repro client code:

var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
socket.Connect("192.168.x.x", 993);

var ssl = new SslStream(new NetworkStream(socket, true), false, (sender, certificate, chain, errors) => true);
var clientCert = X509Certificate.CreateFromCertFile("pathtocertfile");
ssl.AuthenticateAsClient("myserver", new X509CertificateCollection(new [] { clientCert }),
    SslProtocols.Tls12, false);

Also, SslStream.IsMutuallyAuthenticated is false after running the above code on Linux, but true on Windows.

@realityexists What is the value of clientCert.HasPrivateKey? If it's false then there's logic to search the CurrentUser\My and (on non-Linux) LocalMachine\My stores to find a matching cert which has a private key; which you may have installed in Windows but not Linux. If it's true (e.g. the cert file is a PFX with a private key) then it should work since the private key is already known.

@bartonjs you've just helped me find the workaround for this! Thanks very much.

HasPrivateKey is a property on X509Certificate2, while I had a X509Certificate. If I add the line:

clientCert = new X509Certificate2(clientCert);

then authentication succeeds and IsMutuallyAuthenticated = true. (And yes, HasPrivateKey is true.)

This seems like some bizarre behaviour - I'm not sure why it would fail with X509Certificate, but work with X509Certificate2! Once known, though, it's easy to work around.

Why doesn't SslStream throw a descriptive exception to the client in this scenario? The consumer passed a certificate which didn't have a private key. I understand not throwing immediately as the server might not ask for it and that would break existing code, but the point where the server requests the client certificate and the client can't prove it has the private key, when the server aborts the handshake I would expect something to be mentioned about this problem in the exception message.

It did have a private key.

Why doesn't SslStream throw a descriptive exception to the client in this scenario?

I think that the design of SslStream was to allow passing the entirety of store.Certificates from the LM\My or CU\My store. It does some matchy logic to try to find more private keys, then ignores anything that was public-only (and, sometimes, "this cert doesn't chain to something on the server's published trust list") and it doesn't understand "I don't have any client certs" to be an error.

And now, it's "because it hasn't thrown an exception for this ever, so it'd be a breaking change"

Was this page helpful?
0 / 5 - 0 ratings

Related issues

noahfalk picture noahfalk  路  3Comments

jkotas picture jkotas  路  3Comments

yahorsi picture yahorsi  路  3Comments

GitAntoinee picture GitAntoinee  路  3Comments

EgorBo picture EgorBo  路  3Comments