當 Client 與 Server 需要建立一個溝通的管道時可以使用 Socket 的方式建立一個信道,但是使用單純的 Socket 聯機信道可能會擔心傳輸數據的過程中可能被截取修改因而不夠安全,為了防止這種情況我們可以使用建立 SSL Socket 的方式來進行數據的傳輸,所以這篇文章就來說明一下該如何建立 SSL Socket 信道,說實在本人對於憑證這個東西不是很熟悉,雖然在MSDN中已經有范例指導該如何建立 SSL Socket 方法,但是還是在憑證的操作上卡了一下,所以也會將卡住的地方舉出說明以免各位也卡在那兒。
范例將使用 SslStream類別 來說明建立的方法,SslStream 傳輸方式提供了訊息機密性和完整性檢查,當使用 SslStream 時可以防止傳輸的信息被有心人讀取或竄改,使用 SslStream 時需要配合 TcpClient 與 TcpListener 一起使用,當客戶端需要與服務器建立聯機時需要提供X509憑證與服務器的X509憑證進行驗證,SSL 通訊協議協助為使用 SslStream 傳輸的訊息提供機密性和完整性檢查。 當在客戶端和服務器之間進行敏感信息通訊時,應當使用 SSL 連接,例如由SslStream 提供的連接。 使用 SslStream 可以協助防止任何人對在網絡上傳輸的信息進行讀取或竄改,在客戶端與服務器的憑證使用上差別在客戶端使用的憑證不需要包含私鑰(*.cer)而服務器則需要包含私鑰的憑證(*.pfx)。
建立與使用憑證
建立憑證
首先要使用SSL就需要先建立一個憑證,但該如何建立跟使用呢? 可以先參考使用憑證此篇文章。
對於憑證的建立我們可以使用 Makecert.exe 工具,如果有裝 Visual Studio 則可以透過以下方式為建立,「開始」→「所有程序」→「Microsoft Visual Studio 2010」→「Visual Studio Tools」→「Visual Studio 命令提示字符 (2010)」
開啟命令提示字符后輸入:makecert -r -pe -n "CN=SslSocket" -ss My -sky exchange
參數說明如下:
- -r :建立自動簽名的憑證。
- -pe :將產生的私鑰標記為可導出。 如此可在憑證中加入私鑰。
- -n :指定主體的憑證名稱,使用雙引號包覆名稱開頭必須加CN=。
- -ss :指定主體的證書存儲名稱,其儲存輸出憑證,My為證書存儲的個人存放區。
- -sky exchange :指定收受者的密鑰類型,必須是下列之一:signature(表示今要用於數字簽名),exchange(表示密鑰用於密鑰加密和密鑰交換),或一個代表提供程序類型的整數。
詳細的參數說明可以參考此文章Makecert.exe (憑證建立工具)
導出與匯入憑證
在上個步驟中我們已經建立好之后要使用的憑證,接下來就必須將建立好的憑證導出供服務器使用以及匯入到客戶端的計算機中,而詳細的步驟如下。
導出憑證
- 「開始」→「執行」→「輸入MMC」,開啟控制台
- 「檔案」→「新增或移除崁入式管理單元」,在「可用崁入式管理單元」列表中找到憑證后新增到「選取的崁入式管理單元」中
接下來就能夠看到剛剛建立的憑證在個人憑證內
將此憑證導出成包含私鑰憑證與不包含私鑰憑證
匯入憑證
憑證產生完成后就需要將產生的憑證匯入,SslSocket.pfx 之后將提供給服務器使用,而 SslSocket.cer 將提供給客戶端使用,憑證匯入的方式如下。
服務器憑證
將 SslSocket.pfx 放置在項目底下提供程序取用。
客戶端憑證
將 SslSocket.cer 於客戶端計算機使用以下步驟匯入。
開啟「IE」→「工具」→「因特網選項」→「內容」→「憑證」→「受信任的跟證書授權單位」→「匯入」
范例
Step 1
經過以上步驟后憑證的設定已經完成接下來就是要撰寫程序代碼進行測試,首先建立一個 Windows Application 傳案當作 Server 使用。
拉一個窗體窗口出來,如下
產生一個 SslSocket類別 加入以下程序代碼
public sealed class SslSocket { private static TcpListener listener; private static X509Certificate ServerCertificate = null; private static bool IsRun = true; private static string _Certificate = string.Empty; public static string Certificate { get { return _Certificate; } set { _Certificate = value; } } /// <summary> /// 執行服務器監聽 /// </summary> public static void RunServer() { // 建立X509憑證 ServerCertificate = new X509Certificate(Certificate, "ssl"); // 監聽任何IP Address來的訊息 listener = new TcpListener(System.Net.IPAddress.Any, 17170); // 開啟監聽 listener.Start(); while (IsRun) { UpdateStatus(string.Format("{0}-等待客戶端連接", DateTime.Now.ToString("HH:mm:ss"))); TcpClient client = listener.AcceptTcpClient(); if (!IsRun) { listener.Stop(); client.Close(); } else { ProcessClient(client); } } } /// <summary> /// 停止服務器監聽 /// </summary> public static void StopServer() { IsRun = false; UpdateStatus(string.Format("{0}-停止客戶端連接", DateTime.Now.ToString("HH:mm:ss"))); } /// <summary> /// 接收客戶端訊息處理並回覆 /// </summary> /// <param name="pClient"></param> private static void ProcessClient(TcpClient pClient) { SslStream sslStream = new SslStream(pClient.GetStream(), true); try { sslStream.AuthenticateAsServer(ServerCertificate, false, SslProtocols.Tls, true); sslStream.ReadTimeout = 5000; sslStream.WriteTimeout = 5000; UpdateStatus(string.Format("{0}-等待客戶端訊息", DateTime.Now.ToString("HH:mm:ss"))); string messageData = ReadMessage(sslStream); UpdateStatus(string.Format("{0}-接收訊息內容: {1}", DateTime.Now.ToString("HH:mm:ss"), messageData)); byte[] message = Encoding.UTF8.GetBytes(string.Format("服務器已接收此: {0} 訊息<EOF>", messageData)); UpdateStatus(string.Format("{0}-回覆客戶端訊息", DateTime.Now.ToString("HH:mm:ss"))); sslStream.Write(message); } catch (Exception) { sslStream.Close(); pClient.Close(); return; } finally { sslStream.Close(); pClient.Close(); } } /// <summary> /// 讀取訊息內容 /// </summary> /// <param name="pSslStream"></param> /// <returns></returns> private static string ReadMessage(SslStream pSslStream) { byte[] buffer = new byte[2048]; StringBuilder messageData = new StringBuilder(); int bytes = -1; do { bytes = pSslStream.Read(buffer, 0, buffer.Length); Decoder decoder = Encoding.UTF8.GetDecoder(); char[] chars = new char[decoder.GetCharCount(buffer, 0, bytes)]; decoder.GetChars(buffer, 0, bytes, chars, 0); messageData.Append(chars); if (messageData.ToString().IndexOf("<EOF>") != -1) { break; } } while (bytes != 0); return messageData.ToString(); } /// <summary> /// 更新主視窗ListBoxUI /// </summary> /// <param name="pMessage"></param> private static void UpdateStatus(string pMessage) { Form1.MainListBox.Invoke(new Action(() => Form1.MainListBox.Items.Add(pMessage))); } }
此類別中 RunServer 方法為啟動監聽需呼叫的方法,首先將透過 X509Certificate 建構函式 (String, String) 建立一個 X509 憑證存入密鑰容器,傳入參數為 (FileName, Password),之后建立 TcpListener 對象用來監聽來至於 TCP 客戶端的鏈接且在此需要指定本機 IP 及 Port ,而在 While 循環內則建立 TcpClient 對象取得客戶端來連接時的 NetworkStream 的數據流,在此 TcpListener 使用了 AcceptTcpClient (接受暫止聯機要求) 方法,意思是此監聽將處於暫時靜止狀態,當客戶端有鏈接時才會響應,所以最好將 SslSocket 類別使用執行序執行以避免主線程阻塞。
當客戶端已連接后會執行 ProcessClient 方法,此時將建立 SslStream 對象來接收客戶端傳送來的數據流並且進行服務器的憑證驗證,而后進行數據的讀取動作,其中<EOF>卷標為標注訊息的結尾判斷使用,最后將處理完的數據回入數據流中傳送至客戶端處理。
接着在窗體程序中產生一個執行序去執行 RunServer 方法啟動監聽同時還需要指定服務器使用的憑證。
public partial class Form1 : Form { public static ListBox MainListBox; public Form1() { InitializeComponent(); MainListBox = this.lbxMsg; } private void btnStart_Click(object sender, EventArgs e) { Thread socket = new Thread(RunSocket); socket.IsBackground = true; socket.Start(); } private void btnStop_Click(object sender, EventArgs e) { SslSocket.StopServer(); } private void RunSocket() { SslSocket.Certificate = Application.StartupPath + @"\SslSocket.pfx"; SslSocket.RunServer(); } }
Step 2
接下來建立一個客戶端用來連接服務器溝通,建立一個 Web 網站於方案中,將剛剛產生的 SslSocket.cer 憑證放置在網站底下,簡單拉一個測試畫面。
建立一個 SendToServer 類別,主要工作於將客戶端訊息傳送至服務器端,詳細代碼如下。
public class SendToServer { public string HostAddress { get; set; } public int HostPort { get; set; } /// <summary> /// 建構子,傳入服務器IP及Port /// </summary> /// <param name="pHostAddress"></param> /// <param name="pHostPort"></param> public SendToServer(string pHostAddress, int pHostPort) { HostAddress = pHostAddress; HostPort = pHostPort; } /// <summary> /// 執行將訊息發送至服務器方法 /// </summary> /// <param name="pMessage"></param> /// <returns></returns> public string SendMsgToServer(string pMessage) { TcpClient client = new TcpClient(HostAddress, HostPort); SslStream sslStream = new SslStream(client.GetStream(), false, new RemoteCertificateValidationCallback(ValidateServerCertificate), null); X509CertificateCollection certs = new X509CertificateCollection(); X509Certificate cert = X509Certificate.CreateFromCertFile(HttpContext.Current.Server.MapPath(@"~/cer/SslSocket.cer")); certs.Add(cert); try { sslStream.AuthenticateAsClient("SslSocket", certs, System.Security.Authentication.SslProtocols.Tls, true); } catch (Exception ex) { client.Close(); return ex.Message; } byte[] messsage = Encoding.UTF8.GetBytes(string.Format("{0}<EOF>", pMessage)); sslStream.Write(messsage); sslStream.Flush(); string serverMessage = ReadMessage(sslStream); client.Close(); return serverMessage; } /// <summary> /// 讀取訊息內容 /// </summary> /// <param name="pSslStream"></param> /// <returns></returns> private string ReadMessage(SslStream pSslStream) { byte[] buffer = new byte[2048]; StringBuilder messageData = new StringBuilder(); int bytes = -1; do { bytes = pSslStream.Read(buffer, 0, buffer.Length); Decoder decoder = Encoding.UTF8.GetDecoder(); char[] chars = new char[decoder.GetCharCount(buffer, 0, bytes)]; decoder.GetChars(buffer, 0, bytes, chars, 0); messageData.Append(chars); if (messageData.ToString().IndexOf("<EOF>") != -1) { break; } } while (bytes != 0); return messageData.ToString(); } /// <summary> /// 驗證服務器SSL憑證 /// </summary> /// <param name="sender"></param> /// <param name="certificate"></param> /// <param name="chain"></param> /// <param name="sslPolicyErrors"></param> /// <returns></returns> public static bool ValidateServerCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { if (sslPolicyErrors == SslPolicyErrors.None) return true; return false; } }
在 SendMsgToServer 方法中,開始先建立 TcpClient 對象連接至指定的服務器 IP 及 Port ,之后建立 SslStream 對象並透過 AuthenticateAsClient 方法驗證與服務器的憑證,驗證成功后將訊息轉換成 byte[] 寫入數據流中傳送至服務器處理,服務器處理完成后將回寫數據至客戶端進行解析后顯示。
最后在測試頁面程序代碼中,建立一個 SendToServer 對象並且指定其 IP 及 Port 后呼叫 SendMsgToServer 方法即可。
public partial class _Default : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { } protected void btnSend_Click(object sender, EventArgs e) { SendToServer send = new SendToServer("127.0.0.1", 17170); lblResult.Text = send.SendMsgToServer(txtMessage.Text.Trim()); } }
以上為參考MSDN范例而產生的一個簡單的使用說明。