如何在C#中识别服务器名称以供客户端进行服务器身份验证

20

最近我一直在尝试使用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"),但无法使客户端给出的服务器名称与之匹配,从而允许连接。我得到的错误是:

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

编辑:

服务器接受客户端的连接一切正常,但在等待客户端发送消息时超时。(由于证书中的服务器名称与我在客户端提供的名称不同,客户端无法对服务器进行身份验证)这是我的想法,以澄清情况。

更新:

根据答案,我已更改证书制造商为:

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 Exception: 根据验证程序,远程证书无效

出现异常的位置在这里:

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

你是否在客户端使用证书?后面的代码片段中serverName的值是多少?另外,请在客户端验证方法中发布sslPolicyErrors的值。 - Andrey Atapin
6个回答

12
答案可以在SslStream.AuthenticateAsClient Method的“备注”部分找到:

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

如果您为服务器使用的证书的主题是“CN=localhost”,则必须在调用AuthenticateAsClient时将“localhost”作为targetHost参数,以便在客户端成功验证它。如果您将“CN=David-PC”用作证书主题,则必须将AuthenticateAsClient的targetHost设置为“David-PC”。SslStream通过将您打算连接的服务器名称(并将其传递给AuthenticateAsClient)与从服务器接收的证书中的主题进行匹配来检查服务器标识。惯例是运行服务器的计算机名称与证书的主题名称相匹配,在客户端中,您将通过TcpClient打开连接所使用的同一主机名传递给AuthenticateAsClient。

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

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

  • 您没有为证书提供发行者,因此它无法被信任-这是RemoteCertificateChainErrors异常的原因。我建议为开发目的创建一个自签名证书,指定makecert的-r选项。
  • 要使证书受信任,它必须是自签名的并放置在Windows证书存储中的受信任位置,或必须与已经受信任的证书颁发机构的一组签名链接。因此,不要使用“-ss My”选项将证书放置在个人存储中,而应该使用“-ss root”,将其放置在可信根证书颁发机构中,在您的计算机上受信任(从代码中可以看出,客户端正在同一台机器上运行,证书也是在该机器上生成的)。
  • 如果指定要创建的输出文件,makecert将将证书导出为.cer格式,但此格式仅包含公钥,服务器建立SSL连接所需的私钥未包含在内。最简单的方法是在服务器代码中从Windows证书存储区读取证书。(您还可以按照此处描述的方法以其他格式从存储库中导出证书,该格式允许存储私钥导出带有私钥的证书,然后在服务器代码中读取该文件。)

您可以在此处找到有关使用的makecert选项的详细信息Certificate Creation Tool (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。


错误:保存编码证书到存储失败 => 0x5 (5) 生成证书时失败 | 当我尝试使用您指定的参数生成证书时 - Riddlah

5

服务器证书的CN必须与服务器域名完全相同。我想,在您的情况下,公共名称必须是“localhost”(不带引号)。

重要提示:确保,正如您可能已经在其他答案中阅读到的那样,在生产中永远不要使用CN="localhost"


@DavidKroukamp,你可能没有看到我的最后一条评论。你能回答一下吗? - Andrey Atapin

4
首先,不要创建主题为“CN = localhost”或等效内容的证书。它永远不会在生产中使用,所以不要这样做。总是将其发放给计算机的主机名,例如CN =“mycomputer”,在连接时使用主机名而不是localhost。您可以使用“subject alternate name”扩展指定多个名称,但是 makecert 似乎不支持它。
其次,在签发服务器 SSL 证书时,您需要将“服务器身份验证” OID 添加到证书的增强密钥用途(EKU)扩展中。在您的示例中,向 makecert 添加 -eku 1.3.6.1.5.5.7.3.1 参数。如果您想执行客户端证书验证,请使用 OID 1.3.6.1.5.5.7.3.2 的“客户端身份验证”。
最后,makecert 创建的默认证书使用 MD5 作为其哈希算法。虽然它不会影响您的测试,但请养成使用 SHA1 的习惯。在上面的 makecert 参数中添加 -a sha1 来强制使用 SHA1。默认密钥大小也应从 1024 位增加到 2048 位,但您已经了解到这个想法了。

据我所知,现在sha1也不是很安全了...最好尝试使用-a sha256 除此之外,还要强调密钥长度的重要性,因为一些浏览器(chrome?)开始抱怨“弱密钥”->即短密钥和/或使用已知的破解哈希算法。 - Luke
1
@Luke,你说得对,但是旧版本的Windows(XP和2003)不支持使用SHA256(或更好)的证书。这是否是一个问题取决于客户。 - akton
对啊...那里真是一团糟!据我所记,几个月前我可能找到了一种“让系统知道”如何在这些系统上支持新的哈希算法的方法,但这种方法相当笨拙...而且部署起来很麻烦... 此外,有几个版本的makecert.exe文件,旧版本根本不接受sha256参数。我不得不在我的开发PC上的各种VS、SDK和系统文件夹中找出更新的版本... - Luke

1

您尝试过吗?

为完整域名(例如example.net,最好使用example.netexample.comexample.org等不是真实名称的名称)或在实际使用中将要使用的名称(如果这是一个单一站点并且您知道它将是什么)创建证书。

更新您的主机文件,以便它将使用127.0.0.1来访问该名称。


1

1

要使其与WCF配合工作,首先需要创建自签名根授权证书,然后使用它来创建本地主机的证书。

我认为您的项目可能也适用于此,请参阅此文章如何:创建用于开发期间的临时证书以获取详细信息。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接