19

最近、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 例外: 検証手順によるとリモート証明書が無効です

これは私が使用しているコードです。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 例外: 検証手順によるとリモート証明書が無効です

ここで発生しています:

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

6 に答える 6

12

答えはSslStream.AuthenticateAsClient Method Remarks セクションにあります。

targetHost に指定された値は、サーバーの証明書の名前と一致する必要があります。

サブジェクトが "CN=localhost" である証明書をサーバーに使用する場合、クライアント側で正常に認証するには、targetHost パラメーターとして "localhost" を指定して AuthenticateAsClient を呼び出す必要があります。「CN=David-PC」を証明書のサブジェクトとして使用する場合は、「David-PC」を targetHost として AuthenticateAsClient を呼び出す必要があります。SslStream は、接続しようとしている (および AuthenticateAsClient に渡す) サーバー名を、サーバーから受信した証明書のサブジェクトと照合することにより、サーバー ID をチェックします。実際には、サーバーを実行するマシン名は証明書のサブジェクトの名前と一致し、クライアントでは、接続を開くために使用したのと同じホスト名を AuthenticateAsClient に渡します (この場合は TcpClient を使用)。

ただし、サーバーとクライアント間の SSL 接続を正常に確立するための条件は他にもあります。AuthenticateAsServer に渡される証明書には秘密キーが必要であり、クライアント マシンで信頼されている必要があり、SSL セッションの確立に関連するキーの使用制限があってはなりません。

コードサンプルに関連して、問題は証明書の生成と使用に関連しています。

  • 証明書の発行者を提供していないため、信頼できません。これが RemoteCertificateChainErrors Exception の原因です。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(証明書);

サーバーコードで:

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 の代わりにサーバーを実行するマシン名を使用することを検討してください。

于 2012-09-04T09:05:49.373 に答える
5

サーバー証明書の CN は、サーバーのドメイン名とまったく同じでなければなりません。あなたの場合、共通名は「localhost」(引用符なし)でなければならないと思います。

重要:確かに、他の回答で読んだことがあるかもしれませんがCN="localhost"、本番環境では決して使用しないでください。

于 2012-08-27T10:51:37.610 に答える
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して、makecertSHA1 を強制します。デフォルトのキー サイズも 1024 ビットから 2048 ビットに増やす必要がありますが、おわかりでしょう。

于 2012-09-01T12:21:36.630 に答える
1

これを WCF で動作させるには、まず自己署名のルート証明機関証明書を作成し、それを使用して localhost の証明書を作成する必要があります。

あなたのプロジェクトにも同じことが当てはまると思います。詳細については、この記事「方法: 開発中に使用する一時的な証明書を作成する」を参照してください。

于 2012-08-30T07:23:12.637 に答える
1

あなたのアップデートに関して:

SslStream コンストラクターの 1 つを使用すると、 RemoteCertificateValidationCallback delegateを提供できます。提供するメソッドにブレークポイントを配置して、実際に発生しているエラーを確認できるはずです。送信されたSslPolicyErrors値を確認します。

于 2012-09-02T21:40:51.000 に答える
1

やってみました:?

のような完全なドメイン名 (または意図的に実名ではないものexample.netを使用するとよい) のexample.netような完全なドメイン名の証明書を作成するか、それが単一のサイトであり、それが何であるかがわかっている場合にライブで使用される名前を作成します。example.comexample.org

ホスト ファイルを更新して、その名前に 127.0.0.1 を使用するようにします。

于 2012-09-01T10:58:33.470 に答える