2012-08-25 22 views
15

Niedawno próbowałem zrobić serwer SSL/klient zaszyfrowany w C#.Jak zidentyfikować moją nazwę serwera do uwierzytelniania serwera przez klienta w C#

Śledzę this tutorial na MSDN, jednak wymagane zaświadczenie być stworzony dla użycia serwera i klienta przy użyciu makecert.exe więc znalazłem przykład i stworzył certyfikat dobrze:

makecert -sr LocalMachine -ss My -n "CN=Test" -sky exchange -sk 123456 c:/Test.cer

ale teraz problem jest uruchomieniu serwera i czeka na klientów, gdy klient łączy się ona używa nazwy maszynowy który w miarę mogę zebrać jest moje IP w tym przypadku:

127.0.0.1

, a to wymaga nazwy serwerów który musi odpowiadać nazwie serwerów na świadectwie (Test.cer). Próbowałem wiele kombinacji (na przykład „test” „LocalMachine”, „127.0.0.1”, ale nie mogę wydawać się uzyskać klientów podanych nazwa serwera dopasować umożliwiając połączenie Błąd pojawia się:.

Certificate error: RemoteCertificateNameMismatch, RemoteCertificateChainErrors Exception: the remote certificate is invalid according to the validation procedure

oto kod używam to różni się od przykładu MSDN tylko w tym, że ja przypisać ścieżkę certyfikat dla serwera w aplikacji i nazwy komputera i nazwy serwera klienta tOO:

SslTcpServer .cs

using System; 
using System.Collections; 
using System.Net; 
using System.Net.Sockets; 
using System.Net.Security; 
using System.Security.Authentication; 
using System.Text; 
using System.Security.Cryptography.X509Certificates; 
using System.IO; 

namespace Examples.System.Net 
{ 
    public sealed class SslTcpServer 
    { 
     static X509Certificate serverCertificate = null; 
     // The certificate parameter specifies the name of the file 
     // containing the machine certificate. 
     public static void RunServer(string certificate) 
     { 
      serverCertificate = X509Certificate.CreateFromCertFile(certificate); 
      // Create a TCP/IP (IPv4) socket and listen for incoming connections. 
      TcpListener listener = new TcpListener(IPAddress.Any, 8080); 
      listener.Start(); 
      while (true) 
      { 
       Console.WriteLine("Waiting for a client to connect..."); 
       // Application blocks while waiting for an incoming connection. 
       // Type CNTL-C to terminate the server. 
       TcpClient client = listener.AcceptTcpClient(); 
       ProcessClient(client); 
      } 
     } 
     static void ProcessClient(TcpClient client) 
     { 
      // A client has connected. Create the 
      // SslStream using the client's network stream. 
      SslStream sslStream = new SslStream(
       client.GetStream(), false); 
      // Authenticate the server but don't require the client to authenticate. 
      try 
      { 
       sslStream.AuthenticateAsServer(serverCertificate, 
        false, SslProtocols.Tls, true); 
       // Display the properties and settings for the authenticated stream. 
       DisplaySecurityLevel(sslStream); 
       DisplaySecurityServices(sslStream); 
       DisplayCertificateInformation(sslStream); 
       DisplayStreamProperties(sslStream); 

       // Set timeouts for the read and write to 5 seconds. 
       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 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."); 
       sslStream.Close(); 
       client.Close(); 
       return; 
      } 
      finally 
      { 
       // The client stream will be closed with the sslStream 
       // because we specified this behavior when creating 
       // the sslStream. 
       sslStream.Close(); 
       client.Close(); 
      } 
     } 
     static string ReadMessage(SslStream sslStream) 
     { 
      // Read the message sent by the client. 
      // The client signals the end of the message using the 
      // "<EOF>" marker. 
      byte[] buffer = new byte[2048]; 
      StringBuilder messageData = 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); 
       messageData.Append(chars); 
       // Check for EOF or an empty message. 
       if (messageData.ToString().IndexOf("<EOF>") != -1) 
       { 
        break; 
       } 
      } while (bytes != 0); 

      return messageData.ToString(); 
     } 
     static void DisplaySecurityLevel(SslStream stream) 
     { 
      Console.WriteLine("Cipher: {0} strength {1}", stream.CipherAlgorithm, stream.CipherStrength); 
      Console.WriteLine("Hash: {0} strength {1}", stream.HashAlgorithm, stream.HashStrength); 
      Console.WriteLine("Key exchange: {0} strength {1}", stream.KeyExchangeAlgorithm, stream.KeyExchangeStrength); 
      Console.WriteLine("Protocol: {0}", stream.SslProtocol); 
     } 
     static void DisplaySecurityServices(SslStream stream) 
     { 
      Console.WriteLine("Is authenticated: {0} as server? {1}", stream.IsAuthenticated, stream.IsServer); 
      Console.WriteLine("IsSigned: {0}", stream.IsSigned); 
      Console.WriteLine("Is Encrypted: {0}", stream.IsEncrypted); 
     } 
     static void DisplayStreamProperties(SslStream stream) 
     { 
      Console.WriteLine("Can read: {0}, write {1}", stream.CanRead, stream.CanWrite); 
      Console.WriteLine("Can timeout: {0}", stream.CanTimeout); 
     } 
     static void DisplayCertificateInformation(SslStream stream) 
     { 
      Console.WriteLine("Certificate revocation list checked: {0}", stream.CheckCertRevocationStatus); 

      X509Certificate localCertificate = stream.LocalCertificate; 
      if (stream.LocalCertificate != null) 
      { 
       Console.WriteLine("Local cert was issued to {0} and is valid from {1} until {2}.", 
        localCertificate.Subject, 
        localCertificate.GetEffectiveDateString(), 
        localCertificate.GetExpirationDateString()); 
      } 
      else 
      { 
       Console.WriteLine("Local certificate is null."); 
      } 
      // Display the properties of the client's certificate. 
      X509Certificate remoteCertificate = stream.RemoteCertificate; 
      if (stream.RemoteCertificate != null) 
      { 
       Console.WriteLine("Remote cert was issued to {0} and is valid from {1} until {2}.", 
        remoteCertificate.Subject, 
        remoteCertificate.GetEffectiveDateString(), 
        remoteCertificate.GetExpirationDateString()); 
      } 
      else 
      { 
       Console.WriteLine("Remote certificate is null."); 
      } 
     } 
     public static void Main(string[] args) 
     { 
      string certificate = "c:/Test.cer"; 
      SslTcpServer.RunServer(certificate); 
     } 
    } 
} 

SslTcpClient.cs

using System; 
using System.Collections; 
using System.Net; 
using System.Net.Security; 
using System.Net.Sockets; 
using System.Security.Authentication; 
using System.Text; 
using System.Security.Cryptography.X509Certificates; 
using System.IO; 

namespace Examples.System.Net 
{ 
    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) 
     { 
      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, 8080); 
      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(); 
     } 
     public static void Main(string[] args) 
     { 
      string serverCertificateName = null; 
      string machineName = null; 
      /* 
      // 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]; 
      }*/ 
      machineName = "127.0.0.1"; 
      serverCertificateName = "David-PC";// tried Test, LocalMachine and 127.0.0.1 
      SslTcpClient.RunClient(machineName, serverCertificateName); 
      Console.ReadKey(); 
     } 
    } 
} 

EDIT:

Serwer akceptuje połączenie klientów i to wszystko, ale czasy się czekając na klienta, aby wysłać wiadomość. (Klient przyzwyczajenie uwierzytelniania serwera ze względu na nazwę serwera w certyfikacie jest inny od tego, który dostarczył na klienta) dobrze ów myślami na nim tylko w celu wyjaśnienia

UPDATE:

Według Odpowiedź zmieniły ekspres certficiate do:

makecert -sr LocalMachine -ss My -n "CN=localhost" -sky exchange -sk 123456 c:/Test.cer and in my client I have:

 machineName = "127.0.0.1"; 
     serverCertificateName = "localhost";// tried Test, LocalMachine and 127.0.0.1 
     SslTcpClient.RunClient(machineName, serverCertificateName); 

teraz otrzymuję wyjątek:

RemoteCertificateChainErrors Exception: the remote certificate is invalid according to the validation procedure

który jest występujących tutaj:

// 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. "+ e.Message); 
       client.Close(); 
       return; 
      } 
+0

Czy używasz certyfikatu dla klienta? Jaka jest wartość "serverName" w ostatnim fragmencie kodu? Prosimy również o wpisanie wartości 'sslPolicyErrors' w metodzie sprawdzania poprawności klienta. –

Odpowiedz

9

Odpowiedź można znaleźć w sekcji SslStream.AuthenticateAsClient Method Uwagi:.

The value specified for targetHost must match the name on the server's certificate.

Jeśli używasz serwera certyfikat, który jest przedmiotem jest „CN = localhost”, należy zadzwonić AuthenticateAsClient z "localhost" jako parametr targetHost do pomyślnego uwierzytelnienia po stronie klienta. Jeśli używałbyś "CN = David-PC" jako podmiotu certyfikatu, musisz wywołać AuthenticateAsClient z "David-PC" jako targetHost. SslStream sprawdza tożsamość serwera, dopasowując nazwę serwera, który zamierzasz połączyć (i który przekazujesz do AuthenticateAsClient) z tematem w certyfikacie otrzymanym z serwera. Praktyka polega na tym, że nazwa komputera, na którym działa serwer, jest zgodna z nazwą podmiotu certyfikatu, aw kliencie przekazujesz tę samą nazwę hosta do AuthenticateAsClient, jak użyto do otwarcia połączenia (w tym przypadku z TcpClient).

Istnieją jednak inne warunki pomyślnego nawiązania połączenia SSL między serwerami i klientami: certyfikat przekazany do serwera AuthenticateAsServer musi mieć klucz prywatny, musi być zaufany na komputerze klienta i nie może mieć żadnych ograniczeń użycia klucza związanych z używaniem dla ustanawianie sesji SSL.

Teraz, w połączeniu z próbką kodu, problem dotyczy generowania i używania certyfikatu.

  • Nie jesteś dostarczanie emitenta dla swojego świadectwa i w ten sposób nie można zaufać - to jest przyczyną wyjątku RemoteCertificateChainErrors. Proponuję stworzyć samopodpisany certyfikat do celów programistycznych, określający opcję -r makecert.

  • Aby być zaufanym, certyfikat musi być samopodpisany i umieszczony w zaufanej lokalizacji w magazynie certyfikatów systemu Windows lub musi być powiązany łańcuchem podpisów z już zaufanym urzędem certyfikacji. Zamiast opcji -ss Moja opcja, która umieści certyfikat w magazynie osobistym, użyj -s root, który umieści go w zaufanych głównych urzędach certyfikacji i będzie on zaufany na twoim komputerze (z kodu zakładam, że twój klient działa na tym samym komputerze z serwerem, a także generowany jest na nim certyfikat).

  • Jeśli określisz plik wyjściowy do makecert, wyeksportuje certyfikat jako .cer, ale ten format zawiera tylko klucz publiczny, a nie klucz prywatny potrzebny serwerowi do ustanowienia połączenia SSL. Najprostszym sposobem jest odczytanie certyfikatu z magazynu certyfikatów systemu Windows w kodzie serwera. (Można również wyeksportować go ze sklepu w innym formacie, który umożliwia przechowywanie klucza prywatnego zgodnie z opisem tutaj Export a certificate with the private key i odczytać ten plik w kodzie serwera).

można znaleźć szczegółowe informacje na temat opcji MakeCert stosowanych tu Certificate Creation Tool (Makecert.exe)

Podsumowując kod wymaga następujących zmian do uruchomienia (testowane ze swoimi najnowszymi aktualizacjami kod):

  • Użyj następującego polecenie wygenerowania certyfikatu:

makecert -sr LocalMachine -ss root -r -n "CN=localhost" -sky exchange -sk 123456

  • Czytaj certyfikat z systemu Windows Certificate Store zamiast pliku (dla prostoty tym przykładzie), więc zastąpić

serverCertificate = X509Certificate.CreateFromCertFile(certificate);

w kodzie serwera z:

X509Store store = new X509Store(StoreName.Root, StoreLocation.LocalMachine); 
store.Open(OpenFlags.ReadOnly); 
var certificates = store.Certificates.Find(X509FindType.FindBySubjectDistinguishedName, "CN=localhost", false); 
store.Close(); 

if (certificates.Count == 0) 
{ 
    Console.WriteLine("Server certificate not found..."); 
    return; 
} 
else 
{ 
    serverCertificate = certificates[0]; 
} 

Proszę pamiętać, aby zamień "CN = localhost" na temat certyfikatu, który zamierzasz użyć, jeśli później zmienisz kod (w tej sytuacji powinna być ta sama wartość co opcja -n przekazana do makecert). Rozważ także użycie nazwy komputera, który uruchamia serwer zamiast localhost w temacie certyfikatu serwera.

+1

+1 Dziękuję bardzo za rozwiązanie tego problemu :) –

+0

Cieszę się, że pomogłem :) –

5

CN certyfikat serwera musi być dokładnie taka sama, jak nazwa domeny serwera. Przypuszczam, że w twoim przypadku nazwa pospolita musi być "localhost" (bez cudzysłowów).

Ważne: na pewno, jak można przeczytać w innych odpowiedziach, nigdy nie używaj w produkcji CN="localhost".

+0

@DavidKroukamp, ​​być może nie widziałem mojego ostatniego komentarza. Czy możesz udzielić odpowiedzi? –

1

Aby to działało z WCF, należy najpierw utworzyć certyfikat z własnym podpisem użytkownika, a następnie użyć go do utworzenia certyfikatu dla lokalnego hosta.

Myślę, że to samo może dotyczyć również Twojego projektu. Aby uzyskać szczegółowe informacje, zapoznaj się z artykułem How to: Create Temporary Certificates for Use During Development.

1

Czy próbowałeś :?

Tworzenie certyfikatu dla pełnej nazwy domeny jak example.net (dobrze jest użyć example.net, example.com lub example.org do niczego, co nie jest celowo prawdziwa nazwa) lub nazwisko, które będą wykorzystywane w użyciu żywego, czy to jedno miejsce i ty wiesz, co to będzie.

Zaktualizuj plik hosts tak, aby używał 127.0.0.1 dla tej nazwy.

4

Po pierwsze, nie twórz certyfikatu z tematem "CN = localhost" lub odpowiednikiem. Nigdy nie będzie używane w produkcji, więc nie rób tego. Zawsze wystawiaj go na nazwę hosta komputera, np. CN = "mykomputer" i używaj nazwy hosta podczas łączenia się z nim zamiast localhost. Można podać wiele nazw za pomocą rozszerzenia "subject alternate name", ale makecert nie obsługuje go.

Po drugie, wydając certyfikat SSL serwera, należy dodać identyfikator OID "Uwierzytelnianie serwera" do rozszerzonego rozszerzenia użytkowania certyfikatu (EKU). Dodaj parametr -eku 1.3.6.1.5.5.7.3.1 do makecert w swoim przykładzie. Aby wykonać uwierzytelnianie za pomocą certyfikatu klienta, należy użyć identyfikatora OID "uwierzytelnianie klienta" z wersji 1.3.6.1.5.5.7.3.2.

Wreszcie, domyślny certyfikat stworzony przez makecert używa MD5 jako algorytmu mieszania. MD5 jest uważany za niezabezpieczony i, chociaż nie wpłynie to na twoje testy, wejdź w nawyk używania SHA1. Dodaj -a sha1 do powyższych parametrów, aby wymusić SHA1. Domyślny rozmiar klucza również powinien zostać zwiększony z 1024-bitów do 2048-bitów, ale masz pomysł.

+0

O ile mi wiadomo, sha1 też nie jest tak bezpieczne w dzisiejszych czasach ...lepiej wypróbować z -a sha256 Poza tym ważne jest podkreślenie, że ważna jest także długość długiej klawiatury, ponieważ niektóre przeglądarki (chrome?) zaczęły narzekać na "słabe klawisze" -> to jest AFAIK krótkie i/lub używanie znanych-do- be-broken algorytmów haszujących – Luke

+1

@Luke masz rację, ale starsze wersje systemu Windows (XP i 2003) nie obsługują certyfikatów za pomocą SHA256 (lub lepiej). To, czy jest to problem, zależy od klienta. – akton

+0

Dobrze ... jest tam sporo zamieszania! O ile pamiętam, kilka miesięcy temu prawdopodobnie znalazłem sposób, aby "poinformować system", jak obsługiwać nowsze algorytmy haszowania w tych systemach, ale był to dość niezręczny sposób na zrobienie tego .. i koszmar do wdrożenia .. Ponadto istnieje kilka wersji pliku makecert.exe, a starsze w ogóle nie przyjmowały parametru sha256. Musiałem dowiedzieć się nowszych z różnych VS, SDK i folderów systemowych na moim dev PC ... – Luke

Powiązane problemy