如何在c#中通过客户端识别服务器身份validation的服务器名称

我最近一直在尝试用C#创建一个SSL加密的服务器/客户端。

我在MSDN上遵循了本教程,但是,它需要使用makecert.exe为服务器和客户端使用创建证书,所以我找到了一个示例并且它创建了证书:

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

但现在问题是服务器启动并等待客户端,当客户端连接它时使用机器名称 ,在这种情况下,我可以收集的是我的IP:

127.0.0.1

,然后它需要服务器名称 ,该名称必须与证书( Test.cer )上的服务器名称匹配。 我尝试了多种组合(例如“Test”“LocalMachine”,“127.0.0.1”但似乎无法让客户端给出服务器名称以匹配,从而允许连接。我得到的错误是:

证书错误:RemoteCertificateNameMismatch,RemoteCertificateChainErrors例外:根据validation程序,远程证书无效

这里是我使用它的代码与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."); 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 // "" 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("") != -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 "". byte[] messsage = Encoding.UTF8.GetBytes("Hello from the client."); // 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 // "" 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("") != -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(); } } } 

编辑:

服务器接受客户端连接和所有内容,但在等待客户端发送消息时超时。 (由于证书中的服务器名称与我在客户端提供的服务器名称不同,客户端不会对服务器进行身份validation)以及我对此的想法只是为了澄清

更新:

根据答案,我已将证书制作人改为:

makecert -sr LocalMachine -ss My -n“CN = localhost”-sky exchange -sk 123456 c:/Test.cer在我的客户端我有:

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

现在我得到了例外:

RemoteCertificateChainErrors例外:根据validation过程,远程证书无效

这里发生的事情:

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

答案可以在SslStream.AuthenticateAsClient方法备注部分找到:

为targetHost指定的值必须与服务器证书上的名称匹配。

如果您为服务器使用主题为“CN = localhost”的证书,则必须使用“localhost”作为targetHost参数调用AuthenticateAsClient,以在客户端成功对其进行身份validation。 如果您使用“CN = David-PC”作为证书主题,则必须使用“David-PC”作为targetHost调用AuthenticateAsClient。 SslStream通过将要连接的服务器名称(以及传递给AuthenticateAsClient的服务器名称)与从服务器接收的证书中的主题进行匹配来检查服务器标识。 实践是运行服务器的计算机名称与证书主题的名称匹配,在客户端中,您将相同的主机名传递给AuthenticateAsClient,就像您用于打开连接一样(在本例中使用TcpClient)。

但是,在服务器和客户端之间成功建立SSL连接还有其他条件:传递给AuthenticateAsServer的证书必须具有私钥,它必须在客户端计算机上受信任,并且不得具有与建立SSL会话的使用相关的任何密钥使用限制。

现在与您的代码示例相关,您的问题与证书的生成和使用有关。

  • 您没有为证书提供颁发者,因此无法信任 – 这是RemoteCertificateChainErrors例外的原因。 我建议为开发目的创建一个自签名证书,指定makecert的-r选项。

  • 要获得信任,证书必须是自签名的,并且必须放在Windows证书存储区中的受信任位置,或者必须与已签名的证书颁发机构链接。 因此,而不是将证书放在个人存储中的-ss My选项使用-ss root将其置于受信任的根证书颁发机构中,并且它将在您的计算机上受信任(从代码我假设您的客户端正在运行与服务器在同一台机器上,并在其上生成证书)。

  • 如果将输出文件指定为makecert,则会将证书导出为.cer,但此格式仅包含公钥,而不包含服务器建立SSL连接所需的私钥。 最简单的方法是从服务器代码中的Windows证书存储区中读取证书。 (您也可以使用另一种格式从商店中导出它,该格式允许存储私钥,如此处所述使用私钥导出证书并在服务器代码中读取该文件)。

您可以在此找到有关此处使用的makecert选项的详细信息证书创建工具(Makecert.exe)

总之,您的代码需要运行以下更改(使用您的最新代码更新进行测试):

  • 使用以下命令生成证书:

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

如果稍后更改代码,请记住将“CN = localhost”替换为您要使用的证书的主题(在这种情况下,应该与传递给makecert的-n选项的值相同)。 还要考虑在服务器证书的主题中使用运行服务器而不是localhost的计算机名称。

服务器证书的CN必须与服务器的域名完全相同。 我想,在你的情况下,通用名称必须是“localhost”(没有引号)。

重要提示:当然,正如您在其他答案中所读到的那样,在生产中永远不要使用CN="localhost"

首先,不要创建主题为“CN = localhost”或等效的证书。 它永远不会用于生产,所以不要这样做。 始终将其发布到计算机的主机名,例如CN =“mycomputer”,并在连接到主机名而不是localhost时使用主机名。 您可以使用“subject alternate name”扩展名指定多个名称,但makecert似乎不支持它。

其次,在颁发服务器SSL证书时,需要将“服务器身份validation”OID添加到证书的增强型密钥用法(EKU)扩展中。 在-eku 1.3.6.1.5.5.7.3.1参数添加到makecert 。 如果要进行客户端证书身份validation,请使用1.3.6.1.5.5.7.3.2的“客户端身份validation”OID。

最后,makecert创建的默认证书使用MD5作为其散列算法。 MD5被认为是不安全的,虽然它不会影响您的测试,但养成使用SHA1的习惯。 在上面的makecert参数中添加-a sha1以强制使用SHA1。 默认密钥大小也应该从1024位增加到2048位,但你明白了。

要使其与WCF一起使用,必须首先创建自签名根证书颁发机构证书,然后使用它为localhost创建证书。

我认为同样可能也适用于您的项目,请查看本文如何:创建在开发期间使用的临时证书以获取详细信息。

你有没有尝试过:?

example.net创建一个完整的域名证书(对于任何故意不是真名的东西,最好使用example.netexample.comexample.org )或者在实际使用中使用的名称如果是单个网站,你知道它会是什么。

更新您的hosts文件,以便它将使用127.0.0.1作为该名称。

关于您的更新:

其中一个SslStream构造函数允许您提供RemoteCertificateValidationCallback委托 。 您应该能够在您提供的方法中放置一个断点,以查看您获得的实际错误。 检查发送的SslPolicyErrors值。