0

I have made a client-server connection with TLS/SSL handshake (using SSLStream) and I want to make my client to verify the server using a certificate signed by personal CA made by OpenSSL. I have successfully tested it as a localhost connection, however, when I tested it as LAN it failed as it can't verify the chain (obviously, the client machine did not have the cert in the local store). I have made a chain.pem file however.

So here is my question:

  1. How can I verify a remote server without needing to install the CA cert?
  2. If I have to use the chain, how can I use the chain.pem file which has been created using OpenSSL?
  3. If I still have to install the server cert and CA cert to the client, how to install it automatically to client machine?

Here are the code for the server:

namespace Client_Server_SSL2.Server
{
    public sealed class Server
    {
        static X509Certificate2 _ServerCert = null;//Var to store the certificate

        /// <summary>
        /// Start a server and try establishing connection with a certificate request for authentication.
        /// </summary>
        /// <param name="certificate">Name of the file containing the certificate.</param>
        public static void StartServer(string certificate)
        {
            //Create X509Cert from the certificate param.
            _ServerCert = new X509Certificate2(certificate, "password");
                        
            //bool validation = new RemoteCertificateValidationCallback(_ServerCert);
            //Create TCP/IP(IPv4) socket and listen to incoming connection;
            TcpListener listener = new TcpListener(IPAddress.Any,20018);
            listener.Start();

            while (true)//infinite loop to listen
            {
                Console.WriteLine("Waiting for incoming connection...\n" +
                    "Press ctrl + C to terminate!");
                TcpClient client = listener.AcceptTcpClient();//Accept incoming connection
                Client_Proc(client);//Client process and activity.
            }
        }

        /// <summary>
        /// All of the client processes in the server will be runned from here.
        /// </summary>
        /// <param name="client"></param>
        static void Client_Proc(TcpClient client)
        {
            //If client connected, Create SSLStream using Client's network stream.
            SslStream sslStream = new SslStream(client.GetStream(), false);
            
            try
            {
                //Authenticate server but don't require the client to authenticate
                sslStream.AuthenticateAsServer(_ServerCert, clientCertificateRequired: false, checkCertificateRevocation: true);

                //Display properties and setting for authenticated connection. A.K.A. Display Status
                DisplaySecurityLevel(sslStream);
                DisplaySecurityService(sslStream);
                DisplayCertificateInfo(sslStream);
                DisplaySysProp(sslStream);

                //Set timeout for the read and write to 5 sec (5000 in ms).
                sslStream.ReadTimeout = 5000;
                sslStream.WriteTimeout = 5000;
                //Read a message from the client
                Console.WriteLine("Waiting for client message...");
                string messageData = ReadMessage(sslStream);
                Console.WriteLine("Received: {0}", messageData);

                // Write a message to the client.
                byte[] message = Encoding.UTF8.GetBytes("Hello from the server.<EOF>");
                Console.WriteLine("Sending hello message.");
                sslStream.Write(message);
            }
            catch (AuthenticationException msg)//in case of error
            {
                Console.WriteLine("Error exception:" + msg.Message);
                if (msg.InnerException != null)
                {
                    Console.WriteLine("Inner exception: {0}", msg.InnerException.Message);
                }
                Console.WriteLine("Authentication failed - closing the connection.");
                sslStream.Close();
                client.Close();
                return;
            }
            finally//end and close the connection after finishing the session
            {
                sslStream.Close();
                client.Close();
            }
        }

        /// <summary>
        /// Read incoming message from client.
        /// </summary>
        /// <param name="sslStream">Stream to be read</param>
        /// <returns></returns>
        static string ReadMessage(SslStream sslStream)
        {
            //Read Message from client
            // The client signals the end of the message using the
            // "<EOF>" marker.

            byte[] buffer = new byte[2048];
            StringBuilder message = new StringBuilder();
            int bytes_ = -1;
            do
            {
                // Read the client's test message.
                bytes_ = sslStream.Read(buffer, 0, buffer.Length);

                // Use Decoder class to convert from bytes to UTF8
                // in case a character spans two buffers.
                Decoder decoder = Encoding.UTF8.GetDecoder();
                char[] chars = new char[decoder.GetCharCount(buffer, 0, bytes_)];
                decoder.GetChars(buffer, 0, bytes_, chars, 0);
                message.Append(chars);
                // Check for EOF or an empty message.
                if (message.ToString().IndexOf("<EOF>") != -1)
                {
                    break;
                }
            } while (bytes_ != 0);

            return message.ToString();
        }
        /// <summary>
        /// Display the stream security level strength of the cipher, hash and key exchange, along with the protocol.
        /// </summary>
        /// <param name="sslStream">Stream to be displayed</param>
        static void DisplaySecurityLevel(SslStream sslStream)
        {
            Console.WriteLine("Cipher: {0} strength {1}", sslStream.CipherAlgorithm, sslStream.CipherStrength);
            Console.WriteLine("Hash: {0} strength {1}", sslStream.HashAlgorithm, sslStream.HashStrength);
            Console.WriteLine("Key Exchange: {0} strength {1}", sslStream.KeyExchangeAlgorithm, sslStream.KeyExchangeStrength);
            Console.WriteLine("Protocol: {0}", sslStream.SslProtocol);
        }

        /// <summary>
        /// Display runned security of the stream.
        /// </summary>
        /// <param name="sslStream">Stream to be displayed</param>
        static void DisplaySecurityService(SslStream sslStream)
        {
            Console.WriteLine("Is authenticated: {0} server? {1}", sslStream.IsAuthenticated, sslStream.IsServer);
            Console.WriteLine("Is Signed: {0}", sslStream.IsSigned);
            Console.WriteLine("Is Encrypted: {0}",sslStream.IsEncrypted);
        }

        /// <summary>
        /// Display the certificate information of the stream for both server and client.
        /// </summary>
        /// <param name="sslStream">Stream to be displayed</param>
        static void DisplayCertificateInfo(SslStream sslStream)
        {
            //Display the properties of server's certificate
            Console.WriteLine("Certificate revocation list checked: {0}", sslStream.CheckCertRevocationStatus);
            X509Certificate localCertificate = sslStream.LocalCertificate;
            if (sslStream.LocalCertificate != null)
            {
                Console.WriteLine("Local cert is issued to {0}, valid from {1} to {2}.",
                    localCertificate.Subject,
                    localCertificate.GetEffectiveDateString(),
                    localCertificate.GetExpirationDateString());
            } 
            else
            {
                Console.WriteLine("Local cert is not available");
            }
            //Display the properties of client's certificate
            X509Certificate remoteCertificate = sslStream.RemoteCertificate;
            
            if (sslStream.RemoteCertificate != null)
            {
                Console.WriteLine("Remote cert is issued to {0}, valid from {1} to {2}.",
                    remoteCertificate.Subject,
                    remoteCertificate.GetEffectiveDateString(),
                    remoteCertificate.GetExpirationDateString());
            }
            else
            {
                Console.WriteLine("Remote cert is not available");
            }

        }

        static void DisplaySysProp(SslStream sslStream)
        {
            Console.WriteLine("Can read: {0}, write {1}", sslStream.CanRead, sslStream.CanWrite);
            Console.WriteLine("Can timeout: {0}", sslStream.CanTimeout);
        }

        private static void DisplayUsage()
        {
            Console.WriteLine("To start the server specify:");
            Console.WriteLine("serverSync certificateFile.pem/cer/crt");
            Environment.Exit(1);
        }
        public static int Main(string[] args)
        {
            string certificate = null;
            
            string execfileloc = Assembly.GetExecutingAssembly().Location;
            certificate = Path.Combine(Path.GetDirectoryName(execfileloc),"server_cert.p12");
            Console.WriteLine(certificate);
            Console.WriteLine(Dns.GetHostName());
            StartServer(certificate);
            return 0;
        }
    }
}

And here is the client:

namespace Client_Server_SSL2.Client
{
    class Client
    {
        public class SslTcpClient
        {
            private static Hashtable certificateErrors = new Hashtable();

            // The following method is invoked by the RemoteCertificateValidationDelegate.
            public static bool ValidateServerCertificate(
                  object sender,
                  X509Certificate certificate,
                  X509Chain chain,
                  SslPolicyErrors sslPolicyErrors)
            {
                X509Store store = new X509Store("Local", StoreLocation.CurrentUser);

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

                Console.WriteLine("Certificate error: {0}", sslPolicyErrors);

                // Do not allow this client to communicate with unauthenticated servers.
                return false;
            }
            public static void RunClient(string machineName, string serverName)
            {
                // Create a TCP/IP client socket.
                // machineName is the host running the server application.
               
                TcpClient client = new TcpClient(machineName, 20018);
                                
                Console.WriteLine("Client connected.");
                // Create an SSL stream that will close the client's stream.
                SslStream sslStream = new SslStream(
                    client.GetStream(),
                    false,
                    new RemoteCertificateValidationCallback(ValidateServerCertificate),
                    null
                    );
                // The server name must match the name on the server certificate.
                try
                {
                    sslStream.AuthenticateAsClient(serverName);
                }
                catch (AuthenticationException e)
                {
                    Console.WriteLine("Exception: {0}", e.Message);
                    if (e.InnerException != null)
                    {
                        Console.WriteLine("Inner exception: {0}", e.InnerException.Message);
                    }
                    Console.WriteLine("Authentication failed - closing the connection.");
                    client.Close();
                    return;
                }
                // Encode a test message into a byte array.
                // Signal the end of the message using the "<EOF>".
                byte[] messsage = Encoding.UTF8.GetBytes("Hello from the client.<EOF>");
                // Send hello message to the server.
                sslStream.Write(messsage);
                sslStream.Flush();
                // Read message from the server.
                string serverMessage = ReadMessage(sslStream);
                Console.WriteLine("Server says: {0}", serverMessage);
                // Close the client connection.
                client.Close();
                Console.WriteLine("Client closed.");
            }
            static string ReadMessage(SslStream sslStream)
            {
                // Read the  message sent by the server.
                // The end of the message is signaled using the
                // "<EOF>" marker.
                byte[] buffer = new byte[2048];
                StringBuilder messageData = new StringBuilder();
                int bytes = -1;
                do
                {
                    bytes = sslStream.Read(buffer, 0, buffer.Length);

                    // Use Decoder class to convert from bytes to UTF8
                    // in case a character spans two buffers.
                    Decoder decoder = Encoding.UTF8.GetDecoder();
                    char[] chars = new char[decoder.GetCharCount(buffer, 0, bytes)];
                    decoder.GetChars(buffer, 0, bytes, chars, 0);
                    messageData.Append(chars);
                    // Check for EOF.
                    if (messageData.ToString().IndexOf("<EOF>") != -1)
                    {
                        break;
                    }
                } while (bytes != 0);

                return messageData.ToString();
            }
            private static void DisplayUsage()
            {
                Console.WriteLine("To start the client specify:");
                Console.WriteLine("clientSync machineName [serverName]");
                Environment.Exit(1);
                
            }
            
            public static int Main(string[] args)
            {
                string serverCertificateName = null;
                string machineName = null;
                if (args == null || args.Length < 1)
                {
                    DisplayUsage();
                }
                // User can specify the machine name and server name.
                // Server name must match the name on the server's certificate.
                machineName = args[0];
                if (args.Length < 2)
                {
                    serverCertificateName = machineName;
                }
                else
                {
                    serverCertificateName = args[1];
                }
                SslTcpClient.RunClient(machineName, serverCertificateName);
                Console.ReadKey();
                return 0;
            }

        }
    }
}

This code is modified from the msdn guide.

Edit: here is the error I got: System.ComponentModel.Win32Exception (0x80090325): The certificate chain was issued by an authority that is not trusted.

  • You can't. Certificate verification is by definition following the chain until you reach a trusted root. So ideally you would install *either* the CA certificate or the server certificate on the client (you don't need to install both). Barring that you could buy a certificate from a public CA that is trusted by the client. If you ignore certificate errors, you open yourself up to MITM attacks, but that may not be an issue in your situation. You don't need a `RemoteCertificateValidationCallback` unless you are going to do special handling of the certificate, eg ignoring it. – Charlieface Jan 24 '22 at 01:37
  • And how is this all fundamentally different from [your previous question](https://stackoverflow.com/questions/70811197/)? Side point: `sslStream` `TcpClient` and `X509Certificate` all need a `using` to correctly dispose them – Charlieface Jan 24 '22 at 01:40
  • @Charlieface Yes, it is no different, I just want to rewrite the question to make better clarification and plan to remove previous post. Also what do you mean by `using`? Do you mean the one used in the header? I am quite the beginner, so sorry for asking seemingly stupid question. So, in the end, there is no other way to verify using personal CA even using `SSLStore` or `SSLChain`? – Ryugasha I Jan 24 '22 at 01:47
  • In that case, may I ask how to automate the installation of the CA/server certs in the client when deploying it? – Ryugasha I Jan 24 '22 at 01:49
  • Also, thank you for the workaround for this issue. After I installed the CA cert to the client, it does work fine – Ryugasha I Jan 24 '22 at 01:53
  • Deleting and reposting the same question is [heavily frowned upon](https://meta.stackoverflow.com/questions/265233/), please don't do that again. – Charlieface Jan 24 '22 at 01:54
  • [A `using` will correctly dispose/close](https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/using-objects#the-using-statement) streams and sockets etc even in the event of an exception. You must use this on all objects which are `IDisposable`. To install a CA certificate, you can use Group Policy if the client is in the same AD domain. Or you can maybe use your installer to install it. But installing CA certs on machines that you do not own is not advisable, it's much better to purchase a cert from a public CA – Charlieface Jan 24 '22 at 01:55

0 Answers0