niedziela, 10 października 2021

C# - SSL TCP Klasa Serwer, Klient, Generowanie certyfikatu OpenSSL

W tym poście chciałbym opisać proste aplikacje działające jako klient oraz serwer TCP z komunikacją po SSL.


Generowanie certyfikatów OPENSLL:


W celu wygenerowania certyfikatów można użyć program openssl.exe, który jest instalowany razem z pakiem Git dla Windowsa. 

Aby wygenerować certyfikat openssl musi być uruchomiony z uprawnieniami administratora.

Komenda generująca certyfikat to:

  1. req -x509 -newkey rsa:4096 -keyout test/key.pem -out test/cert.pem -days 3650 -sha256


Tak przygotowane certytikaty należy przekonwertować z *.pem na *.pfx. Do tego celu można wykorzystać narzędzie dostępne online https://ssl24.pl/narzedzia-ssl/konwertuj-certyfikat/


Powyżej przykładowa zamiana dla pliku klienta. Taką samą procedurę należy przeprowadzić dla certyfikatu serwera.

Klasa TCP Client:


Poniżej opis działania klienta TCP komunikującego się przez SSL.

Na samym początku po wprowadzeniu ustawień do programu wykonujemy połączenie z urządzeniem:


  1. private void btnConnectWithSSLServ_Click(object sender, EventArgs e)
  2. {
  3.     string serverIpAddress = txtBoxServerIP.Text;
  4.     string portNumber = txtBoxServerPort.Text;
  5.  
  6.     SSL_Connection_Class.sslClient = new SSLClient(serverIpAddress, ConvertStringPortToInt(portNumber), "test_ssl");
  7.     SSL_Connection_Class.sslClient.RunClient();
  8. }

W celu obsługi połączenia SSL pomiędzy poszczególnymi oknami aplikacji wykorzystałem klasę statyczną odpowiedzialną za stworzenie obiektu:

  1. public static class SSL_Connection_Class
  2. {
  3.     public static SSLClient sslClient { get; set; }
  4. }

Tworzeni obiektu klasy może następować na dwa sposoby:

  1. public SSLClient(string serverIpAddress, int serverPort, string targetHost)
  2. {
  3.     this.ServerIpAddress = IPAddress.Parse(serverIpAddress);
  4.     this.ServerPort = serverPort;
  5.     this.TargetHost = targetHost;
  6. }
  7.  
  8. public SSLClient(string serverIpAddress, int serverPort, string targetHost, string clientCertificateFilePath, string clientCertificatePassword)
  9. {
  10.     this.ServerIpAddress = IPAddress.Parse(serverIpAddress);
  11.     this.ServerPort = serverPort;
  12.     this.TargetHost = targetHost;
  13.     this.ClientCertificateFile = clientCertificateFilePath;
  14.     this.ClientCertificatePassword = clientCertificatePassword;
  15. }

W pierwszym przypadku podajemy tylko dane do połączenia, w drugim dokładane są także informacje o certyfikacie, gdy nie jest on wprowadzony bezpośrednio w klasie.

  1. private string ClientCertificateFile = @"C:\\sciezka\\cert.pfx";
  2. private string ClientCertificatePassword = "haslo_do_certyfikatu";

Uruchomienie klienta wygląda następująco:

  1. public void RunClient()
  2. {
  3.     var clientCertificate = new X509Certificate2(ClientCertificateFile, ClientCertificatePassword);
  4.     X509CertificateCollection cert = new X509Certificate2Collection(clientCertificate);
  5.     client = new TcpClient(ServerIpAddress.ToString(), ServerPort);
  6.     Debug.WriteLine("Client connected.");
  7.     sslStream = new SslStream(client.GetStream(), false, new RemoteCertificateValidationCallback(CertificateValidation), null);
  8.  
  9.     try
  10.     {
  11.         sslStream.AuthenticateAsClient(TargetHost, cert, SslProtocols.Tls12, false);
  12.     }
  13.     catch (AuthenticationException e)
  14.     {
  15.         Debug.WriteLine("Exception: {0}", e.Message);
  16.         if (e.InnerException != null)
  17.         {
  18.             Debug.WriteLine("Inner exception: {0}", e.InnerException.Message);
  19.         }
  20.        
  21.         Debug.WriteLine("Authentication failed - closing the connection.");
  22.         client.Close();
  23.         return;
  24.     }
  25.  
  26.     InitializeBackgroundWorker();
  27.     backgroundWorker1.RunWorkerAsync();
  28. }

Na samym początku wykonuje przygotowanie nowej instancji klasy z certyfikatem do której podaje ścieżkę dostępu oraz hasło. Następnie tworzona jest połączenie przez TCP oraz SSL. Do klasy sslStream dodawana jest funkcja sprawdzająca poprawność certyfikatu:

  1. public static bool CertificateValidation(Object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)        {
  2.     if (sslPolicyErrors == SslPolicyErrors.None) { return true; }
  3.     if (sslPolicyErrors == SslPolicyErrors.RemoteCertificateChainErrors) { return true; }
  4.    
  5.     Debug.Write("---" + Environment.NewLine);
  6.     Debug.Write("*** Błd certyfikatu SSL: " + sslPolicyErrors.ToString() + Environment.NewLine);
  7.     Debug.Write("---" + Environment.NewLine);
  8.     Debug.Write("---" + Environment.NewLine);
  9.     Debug.Write("*** Błd certyfikatu SSL: " + sslPolicyErrors.ToString() + Environment.NewLine);
  10.     Debug.Write("---" + Environment.NewLine);
  11.     return false;
  12. }

Funkcja przeprowadzająca sprawdzenie certyfikatu powinna przepuścić błąd RemoteCerificteChainErrors. Jest to wymagane w przypadku tworzenia certyfikatu z podpisem własnym, jaki był to przeze mnie wykorzystywany.

Gdy połączenie oraz sprawdzenie certyfikatu zostało wykonane poprawnie, to zostaje uruchomiona klasa BackgroundWorker, która w osobnym wątku obsługuje wiadomości od i do serwera. W odpowiedzi na otrzymaną wiadomość klient wysyła tą samą ramkę danych.

  1. private void InitializeBackgroundWorker()
  2. {
  3.     backgroundWorker1 = new BackgroundWorker();
  4.     backgroundWorker1.WorkerSupportsCancellation = true;
  5.     backgroundWorker1.DoWork += backgroundWorker1_DoWork;
  6.     backgroundWorker1.RunWorkerCompleted += backgroundWorker1_RunWorkerCompleted;
  7. }
  8.  
  9. private void DisableBackgroundWorker()
  10. {
  11.     backgroundWorker1.DoWork -= backgroundWorker1_DoWork;
  12.     backgroundWorker1.RunWorkerCompleted -= backgroundWorker1_RunWorkerCompleted;
  13. }
  14.  
  15. private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
  16. {
  17.     BackgroundWorker worker = sender as BackgroundWorker;
  18.     e.Result = ReceiveResponse();
  19. }

  1. private int ReceiveResponse()
  2. {
  3.     byte[] buffer = new byte[256];
  4.     int bytes = -1;
  5.  
  6.     do
  7.     {
  8.         try
  9.         {
  10.             bytes = sslStream.Read(buffer, 0, buffer.Length);
  11.         }
  12.         catch
  13.         {
  14.             client.Close();
  15.             return -1;
  16.         }
  17.  
  18.         if(bytes > 0)
  19.         {
  20.             Debug.WriteLine(buffer.ToString());
  21.             Decoder decoder = Encoding.UTF8.GetDecoder();
  22.             char[] chars = new char[decoder.GetCharCount(buffer, 0, bytes)];
  23.             decoder.GetChars(buffer, 0, bytes, chars, 0);
  24.  
  25.             Debug.Write("Receive from server: ");
  26.  
  27.             for (int i = 0; i < chars.Length; i++)
  28.             {
  29.                 Debug.Write(chars[i].ToString());
  30.             }
  31.  
  32.             if (chars[0] == 0x01 && chars[1] == 0xA5 && chars[2] == 0x55)    //Check if close msg
  33.             {
  34.                 client.Close();
  35.             }
  36.  
  37.             bytes = -1;
  38.             Array.Clear(buffer, 0, buffer.Length);
  39.         }
  40.     } while (true);
  41.  
  42.     return 0;
  43. }

Wysłanie komendy wykonuje z osobnego okna:


  1. private void btnSendMsg_Click(object sender, EventArgs e)
  2. {
  3.     string msgToSend = txtBoxMsgToSend.Text;
  4.  
  5.     if (SSL_Connection_Class.sslClient != null)
  6.     {
  7.         bool returnMsg = SSL_Connection_Class.sslClient.SSL_SendMsg(msgToSend + "<EOF>");
  8.  
  9.         if (returnMsg == false)
  10.         {
  11.             MessageBox.Show("Msg can't be send", "ERROR");
  12.         }
  13.     }
  14.     else
  15.     {
  16.         MessageBox.Show("You need to connect with the server first", "ERROR");
  17.     }
  18. }

  1. public bool SSL_SendMsg(string msg)
  2. {
  3.     byte[] messsage = Encoding.UTF8.GetBytes(msg);
  4.  
  5.     try
  6.     {
  7.         sslStream.Write(messsage);
  8.     }
  9.     catch
  10.     {
  11.         return false;
  12.     }
  13.     return true;
  14. }

Klasa TCP Serwer:


Poniżej przykładowy program działający jako serwer TCP z komunikacją po SSL.

Uruchomienie serwera wygląda następująco:

  1. private void btnConnectWithSSLServ_Click(object sender, EventArgs e)
  2. {
  3.     string portNumber = txtBoxServerPort.Text;
  4.    
  5.     SSL_Connection_Class.sslServer = new SSLServer(ConvertStringPortToInt(portNumber));
  6.     new Thread(SSL_Connection_Class.sslServer.RunServer).Start();
  7. }


Wątek obsługujący serwer wygląda następująco:

  1. public void RunServer()
  2. {
  3.     bool checkIfPortIsFree = CheckIfPortIsAvailable(ServerPort);
  4.  
  5.     if(checkIfPortIsFree == false)
  6.     {
  7.         Debug.WriteLine("Port is not available, Check connection.");
  8.         return;
  9.     }
  10.  
  11.     var serverCertificate = new X509Certificate2(ServerCertificateFile, ServerCertificatePassword);
  12.     listener = new TcpListener(IPAddress.Any, ServerPort);
  13.     listener.Start();
  14.  
  15.     try
  16.     {
  17.         using (client = listener.AcceptTcpClient())
  18.         using (sslStream = new SslStream(client.GetStream(), false, App_CertificateValidation))
  19.         {
  20.             sslStream.AuthenticateAsServer(serverCertificate, true, SslProtocols.Tls12, false);
  21.             Debug.WriteLine("---");
  22.             Debug.WriteLine("Połaczenie:              " + client.Client.RemoteEndPoint.ToString());
  23.             Debug.WriteLine("Lokalny certyfikat:      " + sslStream.LocalCertificate.Subject);
  24.             Debug.WriteLine("Zdalny certyfikat:       " + sslStream.RemoteCertificate.Subject);
  25.             Debug.WriteLine("Na porcie:               " + ServerPort);
  26.             Debug.WriteLine("---");
  27.  
  28.             sslStream.ReadTimeout = 60000;
  29.             sslStream.WriteTimeout = 60000;
  30.  
  31.             byte[] inputBuffer = new byte[256];
  32.             int inputBytes = 0;
  33.             Decoder decoder = Encoding.UTF8.GetDecoder();
  34.  
  35.             while (true)
  36.             {
  37.                 inputBytes = -1;
  38.                 Array.Clear(inputBuffer, 0, inputBuffer.Length);
  39.                 if (client.Available > 0)
  40.                 {
  41.                     do
  42.                     {
  43.                         inputBytes = sslStream.Read(inputBuffer, 0, inputBuffer.Length);
  44.                     }while (inputBytes == 0);
  45.  
  46.                     char[] chars = new char[decoder.GetCharCount(inputBuffer, 0, inputBytes)];
  47.                     decoder.GetChars(inputBuffer, 0, inputBytes, chars, 0);
  48.  
  49.                     Debug.Write("Receive from client: ");
  50.  
  51.                     for (int i = 0; i < chars.Length; i++)
  52.                     {
  53.                         Debug.Write(chars[i].ToString());
  54.                     }
  55.  
  56.                     sslStream.Write(inputBuffer, 0, inputBytes);
  57.                 }
  58.             }
  59.         }
  60.     }
  61.     catch(Exception ex)
  62.     {
  63.         Debug.WriteLine("---");
  64.         Debug.WriteLine("*** Problem" + ex.GetType().Name + "  " + ex.Message);
  65.         Debug.WriteLine("---");
  66.     }
  67. }

Przykładowe programy można pobrać z dysku Google pod tym linkiem.