wtorek, 17 listopada 2020

C# - Komunikacja klient serwer SSL

 W tym poście chciałbym opisać sposób komunikacji pomiędzy klientem a serwer przy użyciu SSL. 


[Źródło: https://docs.microsoft.com/en-us/dotnet/]

Klucze:


Dla obu przypadków czyli dla klienta oraz dla serwera należy przygotować odpowiednie certyfikaty.
W moim przypadku certyfikat został wygenerowany z dwóch plików *.pem do jednego pliku *.pfx. 

Do tej generacji można skorzystać z następującej strony

Program Klient:


W programie należy zdefiniować adres IP klienta z którym nastąpi połączenie, port oraz pliki z certyfikatem.

  1. private readonly string Hostname = "hostname";
  2. private readonly int ServerPort = 1234;
  3. private readonly string ServerCertificateFile = @"C:\\iMX6\\...\\\cert.pfx";
  4. private readonly string ServerCertificatePassword = "password";

Następnie uruchamiam osobny wątek dla klienta po kliknięciu w przycisk w którym następuje obsługa wysyłania i odbierania danych pomiędzy klientem oraz serwerem. 

  1. private void StartClientBtn_Click(object sender, EventArgs e)
  2. {
  3.     new Thread(RunClient).Start();
  4.     button1.Enabled = false;
  5. }

Poniżej obsługa nawiązania połączenia z serwerem oraz odebranie i przesłanie wiadomości:

  1. public void RunClient()
  2. {
  3.         var clientCertificate = new X509Certificate2(ClientCertificateFile, ClientCertificatePassword);
  4.         X509Certificate2Collection collection = new X509Certificate2Collection(clientCertificate);
  5.        
  6.         TcpClient client = new TcpClient(Hostname, 5555);
  7.  
  8.         SslStream sslStream = new SslStream(client.GetStream(), false,
  9.                 new RemoteCertificateValidationCallback(ValidateServerCertificate), null);
  10.  
  11.     try
  12.         {
  13.             sslStream.AuthenticateAsClient("rcpkd", collection, SslProtocols.Tls12, false);
  14.         }
  15.         catch (AuthenticationException e)
  16.         {
  17.                 MessageBox.Show("Exception: " + e.Message);
  18.                 if (e.InnerException != null)
  19.                 {
  20.                     MessageBox.Show("Inner exception: " + e.InnerException.Message);
  21.                 }
  22.                 MessageBox.Show("Authentication failed - closing the connection.");
  23.                 client.Close();
  24.                 return;
  25.         }
  26.  
  27.         while(client.Connected)
  28.         {
  29.             string serverMessage = ReadMessage(sslStream);
  30.  
  31.         if(serverMessage.Equals("Close"))
  32.                 {
  33.                     break;
  34.                 }
  35.  
  36.             MessageBox.Show("Server says: " + serverMessage);
  37.  
  38.             if (serverMessage != null && serverMessage != "")
  39.                 {
  40.                     byte[] messsage = Encoding.UTF8.GetBytes("Response MSG\r\n");
  41.                         sslStream.Write(messsage);
  42.                         sslStream.Flush();
  43.                 }
  44.         }
  45.  
  46.         client.Close();
  47.         MessageBox.Show("Client closed.");
  48. }

Powyżej następuje weryfikacja certyfikatu, następnie utworzenie połączenia klienta z serwerem. Dalej utworzony zostaje strumień danych po którym następuje wymiana danych. Metoda AuthenticateAsClient pozwala na uwierzytelnienie połączenia z klientem. Gdy połączenie jest nawiązane następuje wymiana danych pomiędzy programami.

Sprawdzenie poprawności certyfikatu:

Tutaj pojawia się problem generowania błędu RemoteCertificationChainError, który wynika z przygotowania certyfikatu od niezaufanego dostawcy.

Zrzut z wygenerowanego błędu:


Wobec tego należy sprawdzić wystąpienie pozostałych błędów RemoteCertificateNameMismatch oraz RemoteCertificateNotAvailable.

Więc w najprostszej formie sprawdzenie mogło by wyglądać następująco:

  1. public static bool ServerCertificateValidationCallback(object sender,
  2.     X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
  3. {
  4.     if ((sslPolicyErrors & (SslPolicyErrors.None)) > 0) { return true; }
  5.    
  6.     if ((sslPolicyErrors & (SslPolicyErrors.RemoteCertificateNameMismatch)) > 0 ||
  7.             (sslPolicyErrors & (SslPolicyErrors.RemoteCertificateNotAvailable)) > 0)
  8.         {
  9.         return false;
  10.         }
  11.    
  12.         //Return true if RemoteCertificateChainErrors ir no errors
  13.         return true;
  14. }

W powyższym przypadku zakładamy, że wygenerowany błąd dotyczy tylko braku certyfikatu na liście zaufanych dostawców. Natomiast błąd nie koniecznie będzie z tym związany. Wobec tego należałoby sprawdzić czy certyfikat jest poprawny i dopiero zatwierdzić proces walidacji.

  1. private bool ServerCertificateValidationCallback(Object sender, X509Certificate certificate,
  2.     X509Chain chain, SslPolicyErrors sslPolicyErrors)
  3. {
  4.     String rootCAThumbprint = "certificate_CAThumbprint";
  5.  
  6.     if ((sslPolicyErrors & (SslPolicyErrors.None)) > 0) { return true; }
  7.  
  8.     if ((sslPolicyErrors & (SslPolicyErrors.RemoteCertificateNameMismatch)) > 0 ||
  9.             (sslPolicyErrors & (SslPolicyErrors.RemoteCertificateNotAvailable)) > 0)
  10.     {
  11.         return false;
  12.     }
  13.  
  14.     X509Certificate2 projectedRootCert = chain.ChainElements[chain.ChainElements.Count - 1].Certificate;
  15.  
  16.     if (!projectedRootCert.Thumbprint.Equals(rootCAThumbprint))
  17.     {
  18.         return false;
  19.     }
  20.  
  21.     X509Chain customChain = new X509Chain
  22.     {
  23.         ChainPolicy = {
  24.             VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority
  25.         }
  26.     };
  27.  
  28.     bool retValue = customChain.Build(chain.ChainElements[0].Certificate);
  29.  
  30.     customChain.Reset();
  31.     return retValue;
  32. }

Odczytanie wiadomości:

  1. static string ReadMessage(SslStream sslStream)
  2. {
  3.   byte[] buffer = new byte[2048];
  4.         StringBuilder messageData = new StringBuilder();
  5.         int bytes = -1;
  6.         do
  7.         {
  8.         try
  9.         {
  10.               bytes = sslStream.Read(buffer, 0, buffer.Length);
  11.  
  12.                     Decoder decoder = Encoding.UTF8.GetDecoder();
  13.                     char[] chars = new char[decoder.GetCharCount(buffer, 0, bytes)];
  14.  
  15.                     decoder.GetChars(buffer, 0, bytes, chars, 0);
  16.                     messageData.Append(chars);
  17.  
  18.                     if (messageData.ToString().IndexOf("\r") != -1)
  19.                     {
  20.                             break;
  21.                     }
  22.         }
  23.         catch(IOException)
  24.         {
  25.             MessageBox.Show("Connection Closed from server", "Connection closed");
  26.             return "Close";
  27.         }
  28.     } while (bytes != 0);
  29.     return messageData.ToString();
  30. }

Program Serwer:


W tej części opiszę połączenie serwera, wysłanie komendy do klienta oraz obsługę odebranych danych. Weryfikacja certyfikatu jest taka sama jak dla programu klienta.

Podobnie jak wcześniej proces klienta jest uruchamiany w osobnym wątku po kliknięciu w przycisk:

  1. private void SerwerStartBtn_Click(object sender, EventArgs e)
  2. {
  3.     new Thread(Server).Start();
  4. }

Uruchomiony wątek ma za zadania postawienie serwera, przeprowadzenie autentykacji za pomocą certyfikatu, przesłanie danych oraz odebranie odpowiedzi. 

  1. public void Server()
  2. {
  3.     var serverCertificate = new X509Certificate2(ServerCertificateFile, ServerCertificatePassword);
  4.     var listener = new TcpListener(IPAddress.Any, ServerPort);
  5.  
  6.     //Listen for conenction from tcp clients
  7.     listener.Start();
  8.     try
  9.     {
  10.         using (var client = listener.AcceptTcpClient())
  11.         using (var sslStream = new SslStream(client.GetStream(), false, ServerCertificateValidationCallback))
  12.         {
  13.             //Authenticate server
  14.             sslStream.AuthenticateAsServer(serverCertificate, true, SslProtocols.Tls12, false);
  15.  
  16.             //Set timeout
  17.             sslStream.ReadTimeout = 60000;
  18.             sslStream.WriteTimeout = 60000;
  19.  
  20.             byte[] readDataBuffer = new byte[512];
  21.             int RecDataCounter = -1;
  22.  
  23.             while (ServerWorkLoop == 1)
  24.             {
  25.                 do
  26.                 {
  27.                     //Send msg if btn clicked, if btn clicked then this variable is set
  28.                     if (DataToSend == 1)
  29.                     {
  30.                         DataToSend = 0;
  31.                         string msgToSend = "test\r";
  32.                         var outputBuffer = Encoding.UTF8.GetBytes(msgToSend);
  33.                         sslStream.Write(outputBuffer);
  34.                         MessageBox.Show($"Dane wysłane: {msgToSend}", "Dane wysłane");
  35.                     }
  36.                     if (0 < client.Available)
  37.                     {
  38.                         //Read input bytes
  39.                         do
  40.                         {
  41.                             inputBytes = sslStream.Read(readDataBuffer, 0, readDataBuffer.Length);
  42.                         }
  43.                         while (inputBytes == 0);
  44.  
  45.                         inputMessage = Encoding.UTF8.GetString(readDataBuffer, 0, inputBytes);
  46.                         RecDataCounter = inputMessage.IndexOf("\r");
  47.  
  48.                         if (RecDataCounter != -1)
  49.                         {
  50.                             string MsgOut = inputMessage.Substring(0, RecDataCounter);
  51.                             MessageBox.Show($"Otrzymana odpowiedź: {MsgOut}", "Dane wysłane");
  52.                         }
  53.                     }
  54.                 }while (RecDataCounter == -1);
  55.             }
  56.         }
  57.     }
  58.     catch (Exception ex)
  59.     {
  60.         MessageBox.Show($"Error {ex.GetType().Name} - {ex.Message}", "Error");
  61.     }
  62. }