2012-08-25 25 views
15

我最近一直試圖在C#中使用SSL加密的服務器/客戶端。如何識別我的客戶端服務器身份驗證的服務器名稱在c#中

但是我按照MSDN上this教程,它需要一個證書使用makecert.exe所以我找到了一個例子服務器和客戶端使用來創建的,創建優良證書:

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

但現在的問題是在服務器啓動並等待客戶,當客戶端連接它使用機器名其中就我所知是我在這種情況下,IP:

127.0.0.1

,然後它需要服務器名稱必須的證書(Test.cer)在服務器名稱匹配。我曾嘗試多種組合(如「測試」「LOCALMACHINE」,「127.0.0.1」,但不能似乎得到給出服務器名稱客戶相匹配從而使連接我得到的錯誤是:

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

這裏是我正在使用的代碼是從MSDN例中,只有我分配在應用程序服務器和機器名和服務器名的客戶端太證書路徑的主要不同:

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(); 
     } 
    } 
} 

編輯:

服務器接受客戶端的連接和一切,但它超時,同時等待客戶端發送消息。 (客戶端不會與服務器進行身份驗證因證書中的服務器名稱是從我在客戶端提供的一個不同),以及這就是我在上面的想法只是爲了澄清

UPDATE:

根據回答我已經改變了certficiate製作者:

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); 

現在我得到異常:

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

這是發生在這裏:

// 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

您是否在使用客戶端證書?在後面的代碼片段中'serverName'的價值是什麼?另外,請在客戶端的驗證方法中發佈'sslPolicyErrors'的值。 –

回答

9

答案可以在SslStream.AuthenticateAsClient Method備註部分中找到:

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

如果您使用的服務器是誰的主題證書是「CN = localhost」,則必須調用AuthenticateAsClient與「localhost」作爲targetHost參數在客戶端成功驗證它。如果您使用「CN = David-PC」作爲證書主題,則必須使用「David-PC」作爲targetHost調用AuthenticateAsClient。 SslStream通過將您打算連接的服務器名稱(以及您傳遞給AuthenticateAsClient的服務器名稱)與從服務器接收的證書中的主題進行匹配來檢查服務器標識。實踐是,運行服務器的計算機名稱與證書主題的名稱相匹配,並且在客戶端中,您將用於打開連接的相同主機名傳遞給AuthenticateAsClient(在本例中爲TcpClient)。

但是還有其他條件可成功建立服務器和客戶端之間的SSL連接:傳遞給AuthenticateAsServer的證書必須具有私鑰,它必須在客戶端計算機上受信任,並且不得有與使用相關的任何密鑰使用限制建立SSL會話。

現在與您的代碼示例相關,您的問題與證書的生成和使用有關。

  • 您沒有提供您的證書,並通過這種方式就不能信任的發佈者 - 這是RemoteCertificateChainErrors異常的原因。我建議爲開發目的創建一個自簽名證書,指定makecert的-r選項。

  • 要被信任,證書必須是自簽名的,並且必須放置在Windows證書存儲中的受信任位置,或者必須與一連串簽名鏈接到已經信任的證書頒發機構。因此,而不是-ss我的選項,將證書放置在個人存儲使用-ss根,將它放置在受信任的根證書頒發機構,它將在您的機器上受信任(從代碼我假設您的客戶端正在運行與服務器在同一臺計算機上,並在其上生成證書)。

  • 如果您指定一個輸出文件進行makecert,它會將證書導出爲.cer,但此格式僅包含公鑰,而不是服務器建立SSL連接所需的私鑰。最簡單的方法是從服務器代碼中的Windows證書存儲中讀取證書。 (您也可以從商店以另一種格式導出它,以允許存儲私鑰,如Export a certificate with the private key中所述,並在服務器代碼中讀取該文件)。

您可以在這裏進一步瞭解Certificate Creation Tool (Makecert.exe)

總之你的代碼需要進行以下更改運行使用makecert選項的詳細信息(與您的最新代碼更新測試):

  • 使用以下命令以生成證書:

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

  • 閱讀從Windows證書存儲區,而不是文件中的證書(在這個例子中的簡單性),所以在服務器代碼替換爲

serverCertificate = X509Certificate.CreateFromCertFile(certificate);

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]; 
} 

請記住如果稍後更改代碼(在這種情況下應該與傳遞給makecert的-n選項的值相同),將「CN = localhost」替換爲您打算使用的證書的主題。另請考慮在服務器證書的主題中使用運行服務器的計算機名稱,而不是本地主機。

+1

+1非常感謝你解決了這個問題:) –

+0

很高興能有幫助:) –

5

的服務器證書的CN必須是完全一樣的服務器的域名。我想,在你的情況下,通用名稱必須是「本地主機」(沒有引號)。

重要:肯定的,因爲你可能在其他的答案已經閱讀,從來沒有在生產中使用CN="localhost"

+0

@DavidKroukamp,你可能還沒有看到我最後的評論。你能給出答案嗎? –

1

你試過了嗎?

創建一個完整的域名如example.net證書(這是很好的使用example.netexample.comexample.org任何東西那是故意不真名),或將在現場演出中使用的名稱,如果這是一個網站,你知道它會是什麼。

更新您的主機文件,以便它將使用該名稱的127.0.0.1。

4

首先,不要創建主題爲「CN = localhost」或等效的證書。它永遠不會用於生產,所以不要這樣做。始終將其發送到您計算機的主機名,例如CN =「mycomputer」,連接時使用主機名而不是localhost。您可以使用「主題備用名稱」擴展名指定多個名稱,但makecert似乎不支持它。

其次,在頒發服務器SSL證書時,需要將「服務器身份驗證」OID添加到證書的增強型密鑰使用(EKU)擴展中。在您的示例中,將-eku 1.3.6.1.5.5.7.3.1參數添加到makecert。如果要進行客戶端證書認證,請使用1.3.6.1.5.5.7.3.2的「客戶端認證」OID。

最後,由makecert創建的默認證書使用MD5作爲其散列算法。 MD5被認爲是不安全的,雖然它不會影響你的測試,但應養成使用SHA1的習慣。將-a sha1添加到上面的參數makecert以強制SHA1。默認的密鑰大小也應該從1024位增加到2048位,但你明白了。

+0

據我所知,現在sha1也不太安全......更好地嘗試-a sha256 除此之外,還有一點需要強調,keylength也很重要,因爲某些瀏覽器(chrome?)開始抱怨「弱密鑰」 - >這是AFAIK的短小和/或使用已知的密鑰,被破壞的哈希算法 – Luke

+1

@Luke您是正確的,但Windows(XP和2003)的舊版本不支持使用SHA256(或更好)的證書。這是否是一個問題取決於客戶。 – akton

+0

對......那兒真是一團糟! 據我所知,幾個月前,我可能找到了一種「讓系統知道」如何在這些系統上支持更新的哈希算法的方法,但這是一種非常尷尬的方式。 .. 此外還有幾個版本的makecert.exe文件,而舊版本根本不接受sha256參數。我不得不在我的開發PC上的各種VS,SDK和系統文件夾中找出更新的... – Luke

相關問題