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:
- 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:
- private void btnConnectWithSSLServ_Click(object sender, EventArgs e)
- {
- string serverIpAddress = txtBoxServerIP.Text;
- string portNumber = txtBoxServerPort.Text;
- SSL_Connection_Class.sslClient = new SSLClient(serverIpAddress, ConvertStringPortToInt(portNumber), "test_ssl");
- SSL_Connection_Class.sslClient.RunClient();
- }
W celu obsługi połączenia SSL pomiędzy poszczególnymi oknami aplikacji wykorzystałem klasę statyczną odpowiedzialną za stworzenie obiektu:
- public static class SSL_Connection_Class
- {
- public static SSLClient sslClient { get; set; }
- }
Tworzeni obiektu klasy może następować na dwa sposoby:
- public SSLClient(string serverIpAddress, int serverPort, string targetHost)
- {
- this.ServerIpAddress = IPAddress.Parse(serverIpAddress);
- this.ServerPort = serverPort;
- this.TargetHost = targetHost;
- }
- public SSLClient(string serverIpAddress, int serverPort, string targetHost, string clientCertificateFilePath, string clientCertificatePassword)
- {
- this.ServerIpAddress = IPAddress.Parse(serverIpAddress);
- this.ServerPort = serverPort;
- this.TargetHost = targetHost;
- this.ClientCertificateFile = clientCertificateFilePath;
- this.ClientCertificatePassword = clientCertificatePassword;
- }
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.
- private string ClientCertificateFile = @"C:\\sciezka\\cert.pfx";
- private string ClientCertificatePassword = "haslo_do_certyfikatu";
Uruchomienie klienta wygląda następująco:
- public void RunClient()
- {
- var clientCertificate = new X509Certificate2(ClientCertificateFile, ClientCertificatePassword);
- X509CertificateCollection cert = new X509Certificate2Collection(clientCertificate);
- client = new TcpClient(ServerIpAddress.ToString(), ServerPort);
- Debug.WriteLine("Client connected.");
- sslStream = new SslStream(client.GetStream(), false, new RemoteCertificateValidationCallback(CertificateValidation), null);
- try
- {
- sslStream.AuthenticateAsClient(TargetHost, cert, SslProtocols.Tls12, false);
- }
- catch (AuthenticationException e)
- {
- Debug.WriteLine("Exception: {0}", e.Message);
- if (e.InnerException != null)
- {
- Debug.WriteLine("Inner exception: {0}", e.InnerException.Message);
- }
- Debug.WriteLine("Authentication failed - closing the connection.");
- client.Close();
- return;
- }
- InitializeBackgroundWorker();
- backgroundWorker1.RunWorkerAsync();
- }
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:
- public static bool CertificateValidation(Object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) {
- if (sslPolicyErrors == SslPolicyErrors.None) { return true; }
- if (sslPolicyErrors == SslPolicyErrors.RemoteCertificateChainErrors) { return true; }
- Debug.Write("---" + Environment.NewLine);
- Debug.Write("*** Błd certyfikatu SSL: " + sslPolicyErrors.ToString() + Environment.NewLine);
- Debug.Write("---" + Environment.NewLine);
- Debug.Write("---" + Environment.NewLine);
- Debug.Write("*** Błd certyfikatu SSL: " + sslPolicyErrors.ToString() + Environment.NewLine);
- Debug.Write("---" + Environment.NewLine);
- return false;
- }
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.
- private void InitializeBackgroundWorker()
- {
- backgroundWorker1 = new BackgroundWorker();
- backgroundWorker1.WorkerSupportsCancellation = true;
- backgroundWorker1.DoWork += backgroundWorker1_DoWork;
- backgroundWorker1.RunWorkerCompleted += backgroundWorker1_RunWorkerCompleted;
- }
- private void DisableBackgroundWorker()
- {
- backgroundWorker1.DoWork -= backgroundWorker1_DoWork;
- backgroundWorker1.RunWorkerCompleted -= backgroundWorker1_RunWorkerCompleted;
- }
- private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
- {
- BackgroundWorker worker = sender as BackgroundWorker;
- e.Result = ReceiveResponse();
- }
- private int ReceiveResponse()
- {
- byte[] buffer = new byte[256];
- int bytes = -1;
- do
- {
- try
- {
- bytes = sslStream.Read(buffer, 0, buffer.Length);
- }
- catch
- {
- client.Close();
- return -1;
- }
- if(bytes > 0)
- {
- Debug.WriteLine(buffer.ToString());
- Decoder decoder = Encoding.UTF8.GetDecoder();
- char[] chars = new char[decoder.GetCharCount(buffer, 0, bytes)];
- decoder.GetChars(buffer, 0, bytes, chars, 0);
- Debug.Write("Receive from server: ");
- for (int i = 0; i < chars.Length; i++)
- {
- Debug.Write(chars[i].ToString());
- }
- if (chars[0] == 0x01 && chars[1] == 0xA5 && chars[2] == 0x55) //Check if close msg
- {
- client.Close();
- }
- bytes = -1;
- Array.Clear(buffer, 0, buffer.Length);
- }
- } while (true);
- return 0;
- }
Wysłanie komendy wykonuje z osobnego okna:
- private void btnSendMsg_Click(object sender, EventArgs e)
- {
- string msgToSend = txtBoxMsgToSend.Text;
- if (SSL_Connection_Class.sslClient != null)
- {
- bool returnMsg = SSL_Connection_Class.sslClient.SSL_SendMsg(msgToSend + "<EOF>");
- if (returnMsg == false)
- {
- MessageBox.Show("Msg can't be send", "ERROR");
- }
- }
- else
- {
- MessageBox.Show("You need to connect with the server first", "ERROR");
- }
- }
- public bool SSL_SendMsg(string msg)
- {
- byte[] messsage = Encoding.UTF8.GetBytes(msg);
- try
- {
- sslStream.Write(messsage);
- }
- catch
- {
- return false;
- }
- return true;
- }
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:
- private void btnConnectWithSSLServ_Click(object sender, EventArgs e)
- {
- string portNumber = txtBoxServerPort.Text;
- SSL_Connection_Class.sslServer = new SSLServer(ConvertStringPortToInt(portNumber));
- new Thread(SSL_Connection_Class.sslServer.RunServer).Start();
- }
Wątek obsługujący serwer wygląda następująco:
- public void RunServer()
- {
- bool checkIfPortIsFree = CheckIfPortIsAvailable(ServerPort);
- if(checkIfPortIsFree == false)
- {
- Debug.WriteLine("Port is not available, Check connection.");
- return;
- }
- var serverCertificate = new X509Certificate2(ServerCertificateFile, ServerCertificatePassword);
- listener = new TcpListener(IPAddress.Any, ServerPort);
- listener.Start();
- try
- {
- using (client = listener.AcceptTcpClient())
- using (sslStream = new SslStream(client.GetStream(), false, App_CertificateValidation))
- {
- sslStream.AuthenticateAsServer(serverCertificate, true, SslProtocols.Tls12, false);
- Debug.WriteLine("---");
- Debug.WriteLine("Połaczenie: " + client.Client.RemoteEndPoint.ToString());
- Debug.WriteLine("Lokalny certyfikat: " + sslStream.LocalCertificate.Subject);
- Debug.WriteLine("Zdalny certyfikat: " + sslStream.RemoteCertificate.Subject);
- Debug.WriteLine("Na porcie: " + ServerPort);
- Debug.WriteLine("---");
- sslStream.ReadTimeout = 60000;
- sslStream.WriteTimeout = 60000;
- byte[] inputBuffer = new byte[256];
- int inputBytes = 0;
- Decoder decoder = Encoding.UTF8.GetDecoder();
- while (true)
- {
- inputBytes = -1;
- Array.Clear(inputBuffer, 0, inputBuffer.Length);
- if (client.Available > 0)
- {
- do
- {
- inputBytes = sslStream.Read(inputBuffer, 0, inputBuffer.Length);
- }while (inputBytes == 0);
- char[] chars = new char[decoder.GetCharCount(inputBuffer, 0, inputBytes)];
- decoder.GetChars(inputBuffer, 0, inputBytes, chars, 0);
- Debug.Write("Receive from client: ");
- for (int i = 0; i < chars.Length; i++)
- {
- Debug.Write(chars[i].ToString());
- }
- sslStream.Write(inputBuffer, 0, inputBytes);
- }
- }
- }
- }
- catch(Exception ex)
- {
- Debug.WriteLine("---");
- Debug.WriteLine("*** Problem" + ex.GetType().Name + " " + ex.Message);
- Debug.WriteLine("---");
- }
- }
Przykładowe programy można pobrać z dysku Google pod tym linkiem.