2011年11月1日 星期二

使用 SecureSocket 連接 Socket Server

繼上一篇 使用 SecureSocket 連接 HTTPS 後,接下來要測試的是連接 SSL Socket Server。

1.
將上一篇,幫 IIS 安裝的 self-signed certification,匯出 private key 的 *.pfx 檔案 "借" 來使用:



2.
在要執行 socket server 的環境中,安裝 *.pfx,點兩下進行安裝~



安裝憑證的位置,你可以選擇由精靈自己判斷決定:


或自己選擇安裝到 "受信任的根憑證授權單位":


以下,我會先以精靈決定的預設位置進行安裝 (會被安裝到 "個人憑證"),後續也將會遇到一些問題,說明後,會再改將憑證安裝到 "受信任的根憑證授權單位" 以解決下面幾個問題。

3.
安裝後,可於 主控台 (執行 mmc.exe 可叫出) 中,新增管理單元:"憑證",可進行管理作業:


剛提到,預設這個自己簽名的憑證會被安裝到 "個人" 中,若你需要將他移到 "受信任的根憑證授權單位",只要將此憑證 "拖曳" 到不同分類下即可。

Server 憑證準備好了!

4.
接下來要開發 socket server,這裡我直接使用 MSDN 提供的範例:

SslStream 類別
http://msdn.microsoft.com/zh-tw/library/system.net.security.sslstream%28v=VS.100%29.aspx

不過此範例是執行時須帶入憑證檔,我做些調整,改成抓作業系統中已經安裝的憑證檔,

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

            X509Store store =  new X509Store(StoreName.My, StoreLocation.CurrentUser);
            store.Open(OpenFlags.ReadOnly);
            for(int i=0; i<store.Certificates.Count; i++){
                X509Certificate cer = store.Certificates[i];
                trace("[" + i + "]" + cer.Subject);
            }
            X509CertificateCollection collection = store.Certificates.Find(X509FindType.FindBySubjectName, "inner.FlashTeam.com", false);
            trace("X509CertificateCollection.Count=" + collection.Count);
            if (collection.Count == 0)
                return;
            serverCertificate = collection[0];

            // Create a TCP/IP (IPv4) socket and listen for incoming connections.
            TcpListener listener = new TcpListener(IPAddress.Any, 8080);
            listener.Start();
            while (true)
            {
                trace("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.   
                trace("Waiting for client message...");
                string messageData = ReadMessage(sslStream);
                trace("Received: " + messageData);

                // Write a message to the client.
                byte[] message = Encoding.UTF8.GetBytes("Hello from the server.<EOF>");
                trace("Sending hello message.");
                sslStream.Write(message);
            }
            catch (AuthenticationException e)
            {
                trace("Exception: " + e.Message);
                if (e.InnerException != null)
                {
                    trace("Inner exception: " + e.InnerException.Message);
                }
                trace("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)
        {
            trace("===DisplaySecurityLevel===");
            trace("Cipher: " + stream.CipherAlgorithm + ", strength: " + stream.CipherStrength);
            trace("Hash: " + stream.HashAlgorithm + ", strength: " + stream.HashStrength);
            trace("Key exchange: " + stream.KeyExchangeAlgorithm + ", strength: " + stream.KeyExchangeStrength);
            trace("Protocol: " + stream.SslProtocol);
        }
        static void DisplaySecurityServices(SslStream stream)
        {
            trace("===DisplaySecurityServices===");
            trace("Is authenticated: " + stream.IsAuthenticated + ", as server? " + stream.IsServer);
            trace("IsSigned: " + stream.IsSigned);
            trace("Is Encrypted: " + stream.IsEncrypted);
        }
        static void DisplayStreamProperties(SslStream stream)
        {
            trace("===DisplayStreamProperties===");
            trace("Can read: " + stream.CanRead + ", write: " + stream.CanWrite);
            trace("Can timeout: " + stream.CanTimeout);
        }
        static void DisplayCertificateInformation(SslStream stream)
        {
            trace("===DisplayCertificateInformation===");
            trace("Certificate revocation list checked: " + stream.CheckCertRevocationStatus);

            X509Certificate localCertificate = stream.LocalCertificate;
            if (stream.LocalCertificate != null)
            {
                trace("Local cert was issued to " + localCertificate.Subject + " and is valid from " + localCertificate.GetEffectiveDateString() + " until " + localCertificate.GetExpirationDateString() + ".");
            }
            else
            {
                trace("Local certificate is null.");
            }
            // Display the properties of the client's certificate.
            X509Certificate remoteCertificate = stream.RemoteCertificate;
            if (stream.RemoteCertificate != null)
            {
                trace("Remote cert was issued to " + remoteCertificate.Subject + " and is valid from " + remoteCertificate.GetEffectiveDateString() + " until " + remoteCertificate.GetExpirationDateString() + ".");
            }
            else
            {
                trace("Remote certificate is null.");
            }
        }
        private static void DisplayUsage()
        {
            trace("===DisplayUsage===");
            trace("To start the server specify:");
            trace("serverSync certificateFile.cer");
            Environment.Exit(1);
        }
        public static int Main(string[] args)
        {
            string certificate = null;
            /*
            if (args == null || args.Length < 1)
            {
                DisplayUsage();
            }
            certificate = args[0];
            */
            SslTcpServer.RunServer(certificate);
            return 0;
        }

        private static void trace(String msg)
        { 
            String dt = DateTime.Now.ToString("hh:mm:ss");
            Console.WriteLine("[" + dt + "]" + msg);
        }
    }
}


上面程式碼中,需特別注意的地方有:

X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser);



X509CertificateCollection collection = store.Certificates.Find(X509FindType.FindBySubjectName, "inner.FlashTeam.com", false);

決定了從哪種分類中找尋憑證,以及用何種方式搜尋並取得憑證。

5.
準備 flash client 端的程式

TestSecureSocket.as
package
{
 import flash.display.Sprite;
 import flash.events.Event;
 import flash.events.IOErrorEvent;
 import flash.events.ProgressEvent;
 import flash.events.SecurityErrorEvent;
 import flash.net.SecureSocket;
 import flash.net.Socket;
 import flash.system.Security;
 
 public class TestSecureSocket extends Sprite
 {
  private var socket:Socket;

  public function TestSecureSocket()
  {
   trace(SecureSocket.isSupported)
   
   Security.allowDomain("*");
   
   var sslsocket:SecureSocket;
   
   sslsocket = new SecureSocket();
   socket = sslsocket;
   socket.connect("127.0.0.1", 8080);
   
   socket.addEventListener(Event.CONNECT, onConnect);
   socket.addEventListener(Event.CLOSE, onClose);
   socket.addEventListener(IOErrorEvent.IO_ERROR, onError);
   socket.addEventListener(ProgressEvent.SOCKET_DATA, onResponse);
   socket.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onSecError);
  }
  
  private function onConnect(e:Event):void {
   trace("onConnect()");
   socket.writeUTFBytes("GET / HTTP/1.1\n");
   socket.writeUTFBytes("Host: inner.flashteam.com\n");
   socket.writeUTFBytes("<EOF>");
  }
  
  private function onClose(e:Event):void {
   // Security error is thrown if this line is excluded
   trace("onClose()");
   socket.close();
  }
  
  private function onError(e:IOErrorEvent):void {
   trace("onError(), e=" + e);
   
   if(socket is SecureSocket){
    var serverCertificateStatus:String = (socket as SecureSocket).serverCertificateStatus;
    trace("serverCertificateStatus=" + serverCertificateStatus);
   }
  }
  
  private function onSecError(e:SecurityErrorEvent):void {
   trace("onSecError(), e=" + e);
  }
  
  private function onResponse(e:ProgressEvent):void {
   trace("onResponse()");
   
   if (socket.bytesAvailable>0) {
    trace(socket.readUTFBytes(socket.bytesAvailable));
   }
  }
 }
}

因為在 socket server 端,是採用僅針對 server 作認證,不要求 client 端一定要提供認證的作法,所以這裡不用包含認證檔。

6.
整合測試,先啟動 server,再執行 client,

server log 如下畫面:


client log 如下畫面:


可以看到 flash 得到 serverCertificateStatus=untrustedSigners 的 IOErrorEvent。





調整一下 socket server 所在作業系統的憑證安裝位置,移到 "受信任的根憑證授權單位" 中,因為移動了安裝位置,所以 Server 必須調整找憑證的程式碼:

X509Store store = new X509Store(StoreName.Root, StoreLocation.CurrentUser);

然後重新啟動 Server,再重新啟動 flash 測試,得到以下畫面:


可以看到 flash 得到 serverCertificateStatus=principalMismatch 的 IOErrorEvent。

原因是出在 socket.connect("127.0.0.1", 8080),若改為 socket.connect("inner.FlashTeam.com", 8080),便可以看到以下的正確執行結果:

server log:


client log:


7.
步驟 1 的 server 憑證,是從 IIS "借" 來的,若是想要自己透過 openssl 來製作的話,簡單說明如下:

* 安裝 openssl,有人已經編譯好 windows 平台使用的版本
http://www.slproweb.com/products/Win32OpenSSL.html
安裝 Win32 OpenSSL v1.0.0e (8mb)
若有需要的人,需先安裝一下 Visual C++ 2008 Redistributables

* 看到很多文件都提到 linux 平台,需要設定環境變數,所以我想 windows 平台也是,請在環境變數中增加
OPENSSL_CONF = C:\OpenSSL\bin\openssl.cfg

* 準備要來製作憑證,參考以下兩篇文章:

如何製作 SSL X.509 憑證?
http://www.study-area.org/tips/certs/certs.html

Openssl常用命令速查
http://www.myssl.cn/guide/faq_openssl_command.asp

* 開始建立憑證:

參考第一份文件,進行以下三個步驟

openssl genrsa -des3 -out myrootca.key 2048

openssl req -new -key myrootca.key -out myrootca.req

openssl x509 -req -days 3650 -sha1 -extensions v3_ca -signkey myrootca.key -in myrootca.req -out myrootca.crt


參考第二份文件,進行以下三個步驟

openssl req -new -nodes -keyout myrootca.key -out myrootca.csr

openssl x509 -req -days 3650 -sha1 -extensions v3_ca -signkey myrootca.key -in myrootca.csr -out myrootca.cer

openssl pkcs12 -export -out myrootca.pfx -inkey myrootca.key -in myrootca.cer

以上步驟我就不解釋了,我不是專家,怕解釋不清楚,總之最後就可以得到 myrootca.pfx 啦~

3 則留言:

Ben Chang 提到...

這篇的測試,都是本機開發測試,.net socket server 不需要提供 policy file (crossdomain.xml),不過當實際放到跨網域的架構時,即使加入於 port 843 提供 policy file 的相關程式,卻不斷發生一些問題,還在研究中~

Ben Chang 提到...

現在使用 sniffer 觀察的結果,當使用 SecureSocket 時,就連 FP 向 Socket 索取 policy file 時都會使用 SSL,若 843 port 沒有用 SSL 接資料,就會收到亂碼無法處理,接著就會向原本的 port 索取 policy file,若原本的 port 有使用 SSL 來監聽,就可以看到 < policy-file-request /> 。

因為我手邊寫的 .NET server 還有點問題,所以還無法完整確認這點~

Ben Chang 提到...

解法:
使用 SecureSocket 連接 Socket Server - Part 2
http://blog.ben.idv.tw/2011/11/securesocket-socket-server-part-2.html