W tym poście chciałbym opisać sposób komunikacji pomiędzy klientem a serwer przy użyciu SSL.
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.
- private readonly string Hostname = "hostname";
- private readonly int ServerPort = 1234;
- private readonly string ServerCertificateFile = @"C:\\iMX6\\...\\\cert.pfx";
- 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.
- private void StartClientBtn_Click(object sender, EventArgs e)
- {
- new Thread(RunClient).Start();
- button1.Enabled = false;
- }
Poniżej obsługa nawiązania połączenia z serwerem oraz odebranie i przesłanie wiadomości:
- public void RunClient()
- {
- var clientCertificate = new X509Certificate2(ClientCertificateFile, ClientCertificatePassword);
- X509Certificate2Collection collection = new X509Certificate2Collection(clientCertificate);
- TcpClient client = new TcpClient(Hostname, 5555);
- SslStream sslStream = new SslStream(client.GetStream(), false,
- new RemoteCertificateValidationCallback(ValidateServerCertificate), null);
- try
- {
- sslStream.AuthenticateAsClient("rcpkd", collection, SslProtocols.Tls12, false);
- }
- catch (AuthenticationException e)
- {
- MessageBox.Show("Exception: " + e.Message);
- if (e.InnerException != null)
- {
- MessageBox.Show("Inner exception: " + e.InnerException.Message);
- }
- MessageBox.Show("Authentication failed - closing the connection.");
- client.Close();
- return;
- }
- while(client.Connected)
- {
- string serverMessage = ReadMessage(sslStream);
- if(serverMessage.Equals("Close"))
- {
- break;
- }
- MessageBox.Show("Server says: " + serverMessage);
- if (serverMessage != null && serverMessage != "")
- {
- byte[] messsage = Encoding.UTF8.GetBytes("Response MSG\r\n");
- sslStream.Write(messsage);
- sslStream.Flush();
- }
- }
- client.Close();
- MessageBox.Show("Client closed.");
- }
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:
- public static bool ServerCertificateValidationCallback(object sender,
- X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
- {
- if ((sslPolicyErrors & (SslPolicyErrors.None)) > 0) { return true; }
- if ((sslPolicyErrors & (SslPolicyErrors.RemoteCertificateNameMismatch)) > 0 ||
- (sslPolicyErrors & (SslPolicyErrors.RemoteCertificateNotAvailable)) > 0)
- {
- return false;
- }
- //Return true if RemoteCertificateChainErrors ir no errors
- return true;
- }
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.
- private bool ServerCertificateValidationCallback(Object sender, X509Certificate certificate,
- X509Chain chain, SslPolicyErrors sslPolicyErrors)
- {
- String rootCAThumbprint = "certificate_CAThumbprint";
- if ((sslPolicyErrors & (SslPolicyErrors.None)) > 0) { return true; }
- if ((sslPolicyErrors & (SslPolicyErrors.RemoteCertificateNameMismatch)) > 0 ||
- (sslPolicyErrors & (SslPolicyErrors.RemoteCertificateNotAvailable)) > 0)
- {
- return false;
- }
- X509Certificate2 projectedRootCert = chain.ChainElements[chain.ChainElements.Count - 1].Certificate;
- if (!projectedRootCert.Thumbprint.Equals(rootCAThumbprint))
- {
- return false;
- }
- X509Chain customChain = new X509Chain
- {
- ChainPolicy = {
- VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority
- }
- };
- bool retValue = customChain.Build(chain.ChainElements[0].Certificate);
- customChain.Reset();
- return retValue;
- }
Odczytanie wiadomości:
- static string ReadMessage(SslStream sslStream)
- {
byte[] buffer = new byte[2048];
- StringBuilder messageData = new StringBuilder();
- int bytes = -1;
- do
- {
try
{
bytes = sslStream.Read(buffer, 0, buffer.Length);
- Decoder decoder = Encoding.UTF8.GetDecoder();
- char[] chars = new char[decoder.GetCharCount(buffer, 0, bytes)];
- decoder.GetChars(buffer, 0, bytes, chars, 0);
- messageData.Append(chars);
- if (messageData.ToString().IndexOf("\r") != -1)
- {
- break;
- }
}
catch(IOException)
{
MessageBox.Show("Connection Closed from server", "Connection closed");
return "Close";
}
- } while (bytes != 0);
- return messageData.ToString();
- }
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:
- private void SerwerStartBtn_Click(object sender, EventArgs e)
- {
- new Thread(Server).Start();
- }
Uruchomiony wątek ma za zadania postawienie serwera, przeprowadzenie autentykacji za pomocą certyfikatu, przesłanie danych oraz odebranie odpowiedzi.
- public void Server()
- {
- var serverCertificate = new X509Certificate2(ServerCertificateFile, ServerCertificatePassword);
- var listener = new TcpListener(IPAddress.Any, ServerPort);
- //Listen for conenction from tcp clients
- listener.Start();
- try
- {
- using (var client = listener.AcceptTcpClient())
- using (var sslStream = new SslStream(client.GetStream(), false, ServerCertificateValidationCallback))
- {
- //Authenticate server
- sslStream.AuthenticateAsServer(serverCertificate, true, SslProtocols.Tls12, false);
- //Set timeout
- sslStream.ReadTimeout = 60000;
- sslStream.WriteTimeout = 60000;
- byte[] readDataBuffer = new byte[512];
- int RecDataCounter = -1;
- while (ServerWorkLoop == 1)
- {
- do
- {
- //Send msg if btn clicked, if btn clicked then this variable is set
- if (DataToSend == 1)
- {
- DataToSend = 0;
- string msgToSend = "test\r";
- var outputBuffer = Encoding.UTF8.GetBytes(msgToSend);
- sslStream.Write(outputBuffer);
- MessageBox.Show($"Dane wysłane: {msgToSend}", "Dane wysłane");
- }
- if (0 < client.Available)
- {
- //Read input bytes
- do
- {
- inputBytes = sslStream.Read(readDataBuffer, 0, readDataBuffer.Length);
- }
- while (inputBytes == 0);
- inputMessage = Encoding.UTF8.GetString(readDataBuffer, 0, inputBytes);
- RecDataCounter = inputMessage.IndexOf("\r");
- if (RecDataCounter != -1)
- {
- string MsgOut = inputMessage.Substring(0, RecDataCounter);
- MessageBox.Show($"Otrzymana odpowiedź: {MsgOut}", "Dane wysłane");
- }
- }
- }while (RecDataCounter == -1);
- }
- }
- }
- catch (Exception ex)
- {
- MessageBox.Show($"Error {ex.GetType().Name} - {ex.Message}", "Error");
- }
- }