2020年6月13日 星期六

[.NET Core] TLS/SSL Socket programming


 DOTNET Core   Socket   TLS/SSL  






Introduction


This article will shows how to enhance a Socket Server/Client to supports TLS/SSL streaming.

TLS/SSL encrypts the sensitive data being sent between client and server, and authenticate a server.


To enable TLS/SSL streaming in Socket, here are the requirements:

1.  A certificate. (We will use Makecert to create one later.)
2.  Must add the cert to trusted root certificates to bypass “invalid CA“ error or ignore checking the self-signed certificate in client side.

The source code is on my Github: KarateJB/DotNetCore.Socket.Sample, which includes the sample codes of Socket/SSL-Socket server and client.


Environment


.NET Core 3.1.300



Implement



(Optional) Create a self-signed certificate

$ MakeCert -ss Root -sr LocalMachine -a SHA256 -n "CN=127.0.0.1,CN=localhost" -sv local.pvk local.cer -pe -e 12/31/2099 -len 2048
$ pvk2pfx.exe -pvk local.pvk -spc local.cer -pfx local.pfx


The .pfx file (contains public and private keys) is for a Socket server.
The .cer file (contains public key) is for a Socket client.


Server side

First lets create a state-object class for storing buffered streaming data.

SslStramState.cs

public class SslStreamState : IStateObject
{
        /// <summary>
        /// Client Socket
        /// </summary>
        public SslStream SslStream = null;

        private const int FixedBufferSize = 1024;

        /// <summary>
        /// Constructor
        /// </summary>
        public SslStreamState()
        {
            this.Buffer = new byte[this.BufferSize];
            this.Content = new StringBuilder();
        }

        /// <summary>
        /// Size of receive buffer
        /// </summary>
        public int BufferSize
        {
            get { return FixedBufferSize; }
        }

        /// <summary>
        /// Receive buffer
        /// </summary>
        public byte[] Buffer { getset; }

        /// <summary>
        /// Received data string
        /// </summary>
        public StringBuilder Content { getset; }
}



Then create a TCP listener (System.Net.Sockets.TcpListener) to listen for the SSL stream (System.Net.Security.SslStream) as following,


SslSocketServer.cs

using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;

public static class SslSocketServer
{
        public static TcpListener TcpListener = null;

        private const int Port = 6667;
        private const int MaxQueuedClientNumber = 100// Max Queued Clients (that will be waiting for Server to accept and serve)

        static SslSocketServer()
        {
            TcpListener = new TcpListener(System.Net.IPAddress.Any, Port);
        }

        internal static void Start()
        {
            TcpListener.Start(MaxQueuedClientNumber);
        }

        internal static void Stop()
        {
            TcpListener.Stop();
            IsClosed = true;
        }

        internal static void Listen()
        {
            TcpListener.BeginAcceptTcpClient(new AsyncCallback(AcceptCallback), TcpListener);
        }

        private static void AcceptCallback(IAsyncResult ar)
        {
            TcpListener listener = (TcpListener)ar.AsyncState;
            TcpClient handler = listener.EndAcceptTcpClient(ar);

            // SslStream sslStream = new SslStream(client.GetStream(), true);
            var sslStream = new System.Net.Security.SslStream(
                handler.GetStream(), falsenew RemoteCertificateValidationCallback(ValidateServerCertificate), null);

            sslStream.AuthenticateAsServer(new X509Certificate2("Certs/local.pfx", string.Empty), false, SslProtocols.Tls12, false);

            var state = new SslStreamState();
            state.SslStream = sslStream;

            sslStream.BeginRead(state.Buffer, 0, state.BufferSize, new AsyncCallback(ReadCallback), state);
        }

        private static void ReadCallback(IAsyncResult ar)
        {
            var content = string.Empty;

            // Retrieve the state object and the SslStream from the asynchronous state object
            SslStreamState state = (SslStreamState)ar.AsyncState;
            System.Net.Security.SslStream handler = state.SslStream;

            // Read data from the client socket.
            int bytesRead = handler.EndRead(ar);

            if (bytesRead > 0)
            {
                // There  might be more data, so store the data received so far
                state.Content.Append(Encoding.ASCII.GetString(
                    state.Buffer, 0, bytesRead));

                // Check for end-of-file tag. If it is not there, read more data
                content = state.Content.ToString();
                if (content.IndexOf("<EOF>") > -1)
                {
                    // Handle the request (Implement the request handler)
                    using var requestHandler = new MyRequestHandler();
                    requestHandler.HandleAsync(state).Wait();

                    // Echo something back to the client
                    Send(handler, $"Received data on {DateTime.Now.ToString()}");
                }
                else
                {
                    // Not all data received. Get more
                    handler.BeginRead(state.Buffer, 0, state.BufferSize, new AsyncCallback(ReadCallback), state);
                }
            }
        }

        private static void Send(System.Net.Security.SslStream handler, string data)
        {
            // Convert the string data to byte data using ASCII encoding
            byte[] byteData = Encoding.ASCII.GetBytes(data);

            // Begin sending the data to the remote device
            handler.Write(byteData, 0, byteData.Length);
        }

         private static bool ValidateServerCertificate(
            object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
        {
            // For self-signed certificate, return true.
            return true;

            if (sslPolicyErrors == SslPolicyErrors.None)
            {
                return true;
            }

            LoggerProvider.Logger.Error($"Certificate validation error: {sslPolicyErrors}");

            return false;
        }
}



Run the TLS/SSL Socket Server

Here is an example of using the Worker Service to startup the Socket server as following,

public class SslSocketWorker : BackgroundService
{
        protected override async Task ExecuteAsync(CancellationToken cancelToken)
        {
            SslSocketServer.Start();

            while (!cancelToken.IsCancellationRequested)
            {
                SslSocketServer.Listen();
            }

            SslSocketServer.Stop();
         
              await Task.CompletedTask;
        }
    }




Client side

The client side creates a TcpClient(System.Net.Sockets.TcpClient) to establish the SSL streaming.

SslSocketClient.cs

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

public class SslSocketClient
{
        private const string Host = "127.0.0.1";
        private const int Port = 6667;

        protected async Task SendAsync(byte[] clientData)
        {
            TcpClient client = new TcpClient(Host, Port);

            SslStream sslStream =
                new SslStream(client.GetStream(),
                falsenew RemoteCertificateValidationCallback(ValidateServerCertificate), null);

            var certs = new X509Certificate2Collection();
            certs.Add(new X509Certificate2("Certs/local.cer"));

            await sslStream.AuthenticateAsClientAsync("localhost", certs, System.Security.Authentication.SslProtocols.Tls12, false);

            sslStream.Write(clientData);
            sslStream.Flush();

            // Receive response from server
            byte[] rtnBytes = new byte[1024]; // Data buffer for incoming data
            int bytesRec = sslStream.Read(rtnBytes);
            Console.WriteLine($"Echoed from server: {Encoding.ASCII.GetString(rtnBytes, 0, bytesRec)}");
            sslStream.Close();
            client.Close();

            await Task.CompletedTask;
        }

        private static bool ValidateServerCertificate(
            object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
        {
            // For self-signed certificate, always return true.
            return true;

            if (sslPolicyErrors == SslPolicyErrors.None)
            {
                return true;
            }

            return false;
        }
}




An example for sending a message to server and retrieve the respond message back.

MsgSender.cs

public class MsgSender : SslSocketClient
{
        private const string CLIENT_MSG = "Hello, there.";
        public async Task SendMsgAsync()
        {
            // Encode the data string into a byte array
            byte[] byteData = Encoding.ASCII.GetBytes($"{CLIENT_MSG}<EOF>");

            // Send
            await base.SendAsync(byteData);
        }
}






Reference








沒有留言:

張貼留言