2

I recently ran into an issue when converting socket communication to use System.Net.Security.SslStream instead of NetworkStream. This conversion was targeting a solution of .Net Framework 4.8 projects. The requirements of this task dictated mutual TLS authentication. This work proceeded in an uneventful fashion until I converted code in a project that contained both a server and a client. At this point, the behavior was such that the first mutually authenticated connection to occur (whether it was the client or the server) would be successful while the second would always fail. It's important to point out that each process has its own certificate. The exception thrown during the second connection attempt is:

System.Security.Authentication.AuthenticationException: A call to SSPI failed, see inner exception. ---> System.ComponentModel.Win32Exception: The client and server cannot communicate, because they do not possess a common algorithm

After some experimenting I discovered that if I had two unique certificates per process, and used one for all SslStream.AuthenticateAsServer calls and the other for all SslStream.AuthenticateAsClient calls I wouldn't have any issues. Despite finding a way around this issue, I still don't understand why using a single certificate for both a server and client connection is a problem. I cannot find any explanations online.

I created a solution that demonstrates this issue with very little code. The solution contains a class library project with a TlsSample class, and two console apps (AServer and BServer) that try to connect to each other.

TlsSample:

using System;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;

namespace TlsSampleShared
{
    public class TlsSample
    {
        private X509Certificate2Collection certs;
        private int _listenerPort;
        private int _clientPort;
        private string _certificateName;
        private TimeSpan _connectionDelay;

        public TlsSample(int listenerPort, int clientPort, string certificateName, TimeSpan connectionDelay)
        {
            _listenerPort = listenerPort;
            _clientPort = clientPort;
            _certificateName = certificateName;
            _connectionDelay = connectionDelay;
        }

        public async Task ConnectAsync()
        {
            using (var store = new X509Store(StoreName.My, StoreLocation.LocalMachine))
            {
                store.Open(OpenFlags.ReadOnly);
                certs = store.Certificates.Find(X509FindType.FindBySubjectName, _certificateName, true);
                Console.WriteLine($"Using certificate {certs[0].Subject} with thumbnail {certs[0].Thumbprint}.");
            }

            var listenTask = Task.Run(Listen);

            await Task.Delay(_connectionDelay);
            var aClient = new TcpClient();
            aClient.Connect(IPAddress.Loopback, _clientPort);
            var s = new SslStream(aClient.GetStream());
            Authenticate(aClient, s, false);

            await listenTask;
        }

        private async Task Listen()
        {
            var l = new TcpListener(new IPEndPoint(IPAddress.Any, _listenerPort));
            l.Start();
            var c = await l.AcceptTcpClientAsync();
            var s = new SslStream(c.GetStream());
            Authenticate(c, s, true);
        }

        private void Authenticate(TcpClient c, SslStream s, bool server)
        {
            try
            {
                if (server)
                    s.AuthenticateAsServer(certs[0], true, false);
                else
                    s.AuthenticateAsClient(Dns.GetHostName(), certs, false);

                Console.WriteLine($@"TLS {(server ? "server" : "client")} handshake succeeded between ({c.Client.LocalEndPoint}-{c.Client.RemoteEndPoint}):
Authenticated: {s.IsAuthenticated}
Mutually Authenticated: {s.IsMutuallyAuthenticated}
Server: {s.IsServer}
Encrypted: {s.IsEncrypted}
Signed: {s.IsSigned}
Key Exchange Algorithm: {s.KeyExchangeAlgorithm}
Key Exchange Strength: {s.KeyExchangeStrength}
Cipher Algorithm: {s.CipherAlgorithm}
Cipher Strength: {s.CipherStrength}
Hash Algorithm: {s.HashAlgorithm}
Hash Strength: {s.HashStrength}
Ssl Protocol: {s.SslProtocol}");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"An error occurred while authenticating ({c.Client.LocalEndPoint}-{c.Client.RemoteEndPoint}) as a {(server ? "server" : "client")}{Environment.NewLine}{ex}");
            }
        }
    }
}

AServer/Program.cs:

using System;
using System.Threading.Tasks;
using TlsSampleShared;

namespace AServer
{
    class Program
    {
        static async Task Main() => await new TlsSample(7000, 7001, "serverA.VM-MIL-SM", TimeSpan.FromSeconds(1)).ConnectAsync();
    }
}

BServer/Program.cs:

using System;
using System.Threading.Tasks;
using TlsSampleShared;

namespace BServer
{
    class Program
    {
        static async Task Main() => await new TlsSample(7001, 7000, "serverB.VM-MIL-SM", TimeSpan.FromSeconds(2)).ConnectAsync();
    }
}

To experiment you will need to have two certificates in your certificate store. Any insights as to why both client and server authentication using a single certificate fails in the same process would be appreciated.

One additional tidbit of info that I noticed when playing around with my sample is that I get a different exception when AServer and BServer try to connect to each other at essentially the same time. This can be seen by setting the same connection delay for both AServer and BServer. In this scenario, neither connection succeeds and the exception thrown is:

System.Security.Authentication.AuthenticationException: A call to SSPI failed, see inner exception. ---> System.ComponentModel.Win32Exception: The Local Security Authority cannot be contacted
smarcaurele
  • 425
  • 3
  • 9

1 Answers1

1

Microsoft confirmed this behavior to be a bug (at least in .Net Framework 4.8). The bug does not exist in the SslStream class in .Net 5.

smarcaurele
  • 425
  • 3
  • 9