2012-08-25 20 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

相关问题