2012-08-25 40 views
15

Gần đây tôi đã cố gắng thực hiện Máy chủ/Máy khách được mã hóa SSL trong C#.Làm cách nào để xác định tên máy chủ để xác thực máy chủ của khách hàng bằng C#

Tôi đã theo this hướng dẫn trên MSDN, tuy nhiên, nó đòi hỏi một chứng chỉ được tạo ra cho các máy chủ và máy khách sử dụng sử dụng makecert.exe vì vậy tôi tìm thấy một ví dụ và nó tạo ra các giấy chứng nhận tốt:

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

nhưng bây giờ vấn đề là máy chủ bắt đầu và chờ đợi cho khách hàng, khi khách hàng kết nối nó sử dụng tên máy mà như xa như tôi có thể thu thập là IP của tôi trong trường hợp này:

127.0.0.1

và sau đó yêu cầu tên máy chủ phải khớp với tên máy chủ trên chứng chỉ (Test.cer). Tôi đã thử nhiều kết hợp (chẳng hạn như "Test" "LocalMachine", "127.0.0.1", nhưng không thể dường như để có được khách hàng cho tên máy chủ để phù hợp do đó cho phép kết nối Các lỗi tôi nhận được là:.

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

đây là mã tôi đang sử dụng nó khác với ví dụ MSDN chỉ trong thực tế là tôi gán đường dẫn chứng chỉ máy chủ trong ứng dụng và tên máy và tên máy chủ của khách hàng quá:

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:

Máy chủ chấp nhận các kết nối khách hàng và tất cả mọi thứ nhưng nó lần ra trong khi chờ đợi cho khách hàng để gửi một tin nhắn. (Khách hàng sẽ không xác thực với máy chủ do tên máy chủ trong giấy chứng nhận là khác nhau từ người tôi cung cấp trong client) thats cũng suy nghĩ của tôi về nó chỉ để làm rõ

UPDATE:

Theo một câu trả lời tôi đã thay đổi nhà sản xuất certficiate tới:

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

bây giờ tôi có được ngoại lệ:

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

đó đang xảy ra ở đây:

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

Bạn có đang sử dụng chứng chỉ cho khách hàng không? Giá trị của 'serverName' trong đoạn mã sau là gì? Ngoài ra, vui lòng đăng giá trị 'sslPolicyErrors' trong phương thức xác thực của ứng dụng khách. –

Trả lời

9

Câu trả lời có thể được tìm thấy tại SslStream.AuthenticateAsClient Method Các chú thích phần:.

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

Nếu bạn sử dụng cho các máy chủ giấy chứng nhận ai là chủ đề là "CN = localhost", bạn phải gọi AuthenticateAsClient với "localhost" làm tham số targetHost để xác thực thành công nó ở phía máy khách. Nếu bạn sử dụng "CN = David-PC" làm chủ đề chứng chỉ, bạn phải gọi AuthenticateAsClient bằng "David-PC" làm targetHost. SslStream kiểm tra danh tính máy chủ bằng cách khớp tên máy chủ mà bạn định kết nối (và bạn chuyển đến AuthenticateAsClient) với chủ đề trong chứng chỉ nhận được từ máy chủ. Thực tế là tên máy chạy máy chủ khớp với tên của chủ thể của chứng chỉ, và trong máy khách bạn chuyển cùng một tên máy chủ cho AuthenticateAsClient như bạn đã sử dụng để mở kết nối (với TcpClient trong trường hợp này).Tuy nhiên, có các điều kiện khác để thiết lập thành công kết nối SSL giữa máy chủ và máy khách: chứng chỉ được chuyển đến AuthenticateAsServer phải có khóa riêng, phải được tin cậy trên máy khách và không có bất kỳ hạn chế sử dụng khóa nào liên quan đến việc sử dụng thiết lập các phiên SSL.

Bây giờ liên quan đến mẫu mã của bạn, vấn đề của bạn liên quan đến việc tạo và sử dụng chứng chỉ.

  • Bạn chưa cung cấp một tổ chức phát hành đối với chứng chỉ của bạn và theo cách này nó không thể tin tưởng - đây là nguyên nhân của RemoteCertificateChainErrors ngoại lệ. Tôi đề nghị tạo một chứng chỉ tự ký cho các mục đích phát triển xác định tùy chọn -r của makecert.

  • Để được tin cậy, chứng chỉ phải được ký tự và được đặt ở vị trí đáng tin cậy trong Cửa hàng chứng chỉ Windows hoặc phải được liên kết với một chuỗi chữ ký đến Tổ chức phát hành chứng chỉ đã tin cậy. Vì vậy, thay vì tùy chọn -ss My sẽ đặt chứng chỉ trong cửa hàng Personal -ss root sẽ đặt nó trong Trusted Root Certification Authorities và nó sẽ được tin cậy trên máy của bạn (từ mã tôi giả định rằng máy khách của bạn đang chạy trên cùng một máy với máy chủ và cũng là chứng chỉ được tạo ra trên nó).

  • Nếu bạn chỉ định tệp đầu ra để xác nhận nó sẽ xuất chứng chỉ dưới dạng .cer nhưng định dạng này chỉ chứa khóa công khai, không phải khóa cá nhân cần thiết bởi máy chủ để thiết lập kết nối SSL. Cách dễ nhất là đọc chứng chỉ từ kho chứng chỉ Windows trong mã máy chủ. (Bạn cũng có thể xuất nó từ cửa hàng ở định dạng khác cho phép lưu trữ khóa riêng như được mô tả ở đây Export a certificate with the private key và đọc tệp đó trong mã máy chủ).

Bạn có thể tìm thông tin chi tiết về các tùy chọn makecert sử dụng ở đây Certificate Creation Tool (Makecert.exe)

Tóm lại mã của bạn cần những thay đổi sau đây để chạy (thử nghiệm với bản cập nhật mã mới nhất của bạn):

  • Sử dụng như sau lệnh để tạo chứng chỉ:

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

  • Đọc giấy chứng nhận từ Windows Certificate Store thay vì một tập tin (đối với sự đơn giản của ví dụ này), vì vậy thay

serverCertificate = X509Certificate.CreateFromCertFile(certificate);

trong mã máy chủ với:

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

Hãy nhớ thay thế "CN = localhost" bằng chủ đề của chứng chỉ mà bạn dự định sử dụng nếu bạn thay đổi mã sau (trong trường hợp này phải có cùng giá trị với tùy chọn -n được chuyển tới makecert). Cũng nên xem xét sử dụng tên máy chạy máy chủ thay vì localhost trong chủ đề của chứng chỉ máy chủ.

+1

+1 Cảm ơn bạn rất nhiều điều này đã giải quyết được vấn đề :) –

+0

Rất vui khi được trợ giúp :) –

5

CN Một chứng chỉ máy chủ của phải chính xác giống như tên miền của máy chủ. Tôi cho rằng, trong trường hợp của bạn, tên thông thường phải là "localhost" (w/o quotes).

Quan trọng: chắc chắn, như bạn có thể đã đọc trong các câu trả lời khác, không bao giờ sử dụng CN="localhost" trong quá trình sản xuất.

+0

@DavidKroukamp, ​​bạn có thể chưa thấy nhận xét cuối cùng của tôi. Bạn có thể đưa ra câu trả lời không? –

1

Để làm việc này với WCF, cần phải tạo chứng chỉ quyền root gốc tự ký và sau đó sử dụng nó để tạo chứng chỉ cho máy chủ cục bộ.

Tôi nghĩ điều tương tự cũng có thể áp dụng cho dự án của bạn, vui lòng xem bài viết này How to: Create Temporary Certificates for Use During Development để biết chi tiết.

1

Bạn đã thử :?

Tạo chứng chỉ cho một tên miền đầy đủ như example.net (đó là tốt để sử dụng example.net, example.com hoặc example.org cho bất cứ điều gì đó là cố tình không phải là một tên thật) hoặc tên mà sẽ được sử dụng trong việc sử dụng trực tiếp nếu đó là một trang web duy nhất và bạn biết nó sẽ là gì.

Cập nhật tệp lưu trữ của bạn để tệp sẽ sử dụng 127.0.0.1 cho tên đó.

4

Trước tiên, không tạo chứng chỉ với chủ đề "CN = localhost" hoặc tương đương. Nó sẽ không bao giờ được sử dụng trong sản xuất vì vậy đừng làm điều đó. Luôn phát hành nó cho tên máy chủ của máy tính của bạn, ví dụ: CN = "mycomputer" và sử dụng tên máy chủ khi kết nối với nó thay vì localhost. Bạn có thể chỉ định nhiều tên bằng cách sử dụng phần mở rộng "tên thay thế chủ đề" nhưng makecert dường như không hỗ trợ.

Thứ hai, khi phát hành chứng chỉ SSL máy chủ, bạn cần phải thêm OID "máy chủ xác thực" vào phần mở rộng sử dụng khóa nâng cao (EKU) của chứng chỉ. Thêm thông số -eku 1.3.6.1.5.5.7.3.1 vào makecert trong ví dụ của bạn. Nếu bạn muốn xác thực chứng chỉ ứng dụng khách, hãy sử dụng OID "xác thực ứng dụng khách" của 1.3.6.1.5.5.7.3.2.

Cuối cùng, chứng chỉ mặc định được tạo bởi makecert sử dụng MD5 làm thuật toán băm của nó. MD5 được coi là không an toàn và, mặc dù nó sẽ không ảnh hưởng đến thử nghiệm của bạn, có thói quen sử dụng SHA1. Thêm -a sha1 vào các thông số makecert ở trên để buộc SHA1. Kích thước khóa mặc định cũng sẽ được tăng từ 1024 bit lên 2048 bit nhưng bạn có ý tưởng.

+0

Theo như tôi biết, sha1 cũng không an toàn như vậy ngày nay ...Ngoài ra, điều quan trọng là phải nhấn mạnh rằng keylength là quan trọng, vì một số trình duyệt (chrome?) bắt đầu phàn nàn về "phím yếu" -> đó là AFAIK ngắn và/hoặc sử dụng được biết đến các thuật toán bẻ khóa bị vỡ – Luke

+1

@Luke bạn là chính xác nhưng các phiên bản cũ hơn của Windows (XP và 2003) không hỗ trợ chứng chỉ bằng SHA256 (hoặc tốt hơn). Cho dù đây là một vấn đề phụ thuộc vào khách hàng. – akton

+0

Phải ... có một mớ hỗn độn ở đó! Theo như tôi nhớ, một vài tháng trước, tôi có thể tìm ra cách để "cho hệ thống biết" cách hỗ trợ các thuật toán băm mới hơn trên các hệ thống đó, nhưng đó là một cách khá khó xử để làm điều đó .. và một cơn ác mộng để triển khai .. Hơn nữa, có một số phiên bản của tệp makecert.exe và các phiên bản cũ hơn không chấp nhận tham số sha256. Tôi phải tìm ra những cái mới hơn trong số các thư mục VS, SDK và hệ thống khác nhau trên máy tính ... – Luke

Các vấn đề liên quan