本文將詳細介紹如何在Java端、C++端和NodeJs端實現基於SSL/TLS的加密通信,重點分析Java端利用SocketChannel和SSLEngine從握手到數據發送/接收的完整過程。本文也涵蓋了在Ubuntu系統上利用OpenSSL和Libevent如何創建一個支持SSL的服務端。文章中介 ...
本文將詳細介紹如何在Java端、C++端和NodeJs端實現基於SSL/TLS的加密通信,重點分析Java端利用SocketChannel和SSLEngine從握手到數據發送/接收的完整過程。本文也涵蓋了在Ubuntu系統上利用OpenSSL和Libevent如何創建一個支持SSL的服務端。文章中介紹的知識點並未全部在SMSS項目中實現,因此筆者會列出所有相關源碼以方便讀者查閱。提醒:由於知識點較多,分享涵蓋了多種語言。預計的學習時間可能會大於3小時,為了保證讀者能有良好的學習體驗,繼續前請先安排好時間。如果遇到困難,您也可以根據自己的實際情況有選擇的學習,也歡迎與我交流。
一 相關前置知識
libevent網路庫:libevent是一個用c語言編寫的高性能支持事件響應的網路庫,編譯libevent前需要確保目標機器上已經完成對openssl的編譯。否則生成的動態庫中可能會缺少調用openssl的介面。這裡選擇的openssl版本為1.1.1d,如果你選擇1.0以前的版本可能與後面的代碼示例有所不同。
electron桌面應用:electron是一套依賴google的V8引擎直接使用HTML/JS/CSS創建桌面應用的跨平臺解決方案。如果你需要開發輕量化的桌面端應用,electron基本是不二選擇。從個人的實踐來看,無論是開發生態還是開發效率都強於Qt。使用electron可以調用nodejs相關介面完成與系統的交互。
Java-nio開發包:基本是現在作為Java中高級開發的必備技能。
javax.net.ssl開發包:屬於Java對SSL/TLS支持的比較底層的開發包。目前在應用中更多會選擇Netty等集成式框架,如果你的項目中需要一些定製化功能可以選擇它作為支持。建議在項目中慎重使用。由於一些特殊原因,Java只提供了SSLSocket對象,底層只支持阻塞式訪問。文章最後會提供一個我個人實現的SSLSocketChannel對象,方便讀者在基礎上進行二次封裝。
SSL/TLS通信:安全通信的目的是在原有的tcp/ip層和應用層之間增加了一個稱之為SSL/TLS的加/解密層來實現的。在網路協議層中的位置大致如下:
在OSI七層網路協議的定義中,它處於表示層。程式開發的方式一般是在完成tcp/ip建立連接後,開始ssl/tls握手。發佈ssl的服務端需要具備一個私鑰文件(.key)以及與私鑰配套的證書文件(.crt)。證書包含了公鑰和對公鑰的簽名,還有一些用來證明源安全的信息。證書需要到專門的機構申請並且有年費要求,鑒於各位讀者僅用於自學,後面生成的證書我們會做自簽名。ssl/tls握手的目的是在客戶端和服務端之間協商一個安全的對稱秘鑰,用來為本次會話的消息加解密,由於這對秘鑰僅通信的服務端和客戶端持有,會話結束即消失。
二 libevent和openssl
生成x.509證書
首選在安裝好openssl的機器上創建私鑰文件:server.key
> openssl genrsa -out server.key 2048
得到私鑰文件後我們需要一個證書請求文件:server.csr,將來你可以拿這個證書請求向正規的證書管理機構申請證書
> openssl req -new -key server.key -out server.csr
最後我們生成自簽名的x.509證書(有效期365天):server.crt
> openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
x.509證書是密碼學里公鑰證書的格式標準,被應用在包括ssl/tls等多項場景中。
OpenSSL加密通信介面分析
與ssl/tls通信相關的介面基本可以分為兩大類,SSL_CTX通信上下文和SSL直接通信介面,下麵逐一分析:
- SSL_CTX_new:新版本摒棄了一些老的介面,目前建議基本統一使用此方法來創建通信上下文
- SSL_CTX_free:釋放SSL_CTX*
- SSL_CTX_use_certificate_file:設置證書文件
- SSL_CTX_use_PrivateKey_file:設置私鑰文件,與上面的證書文件必須配套否則檢測不通過
- SSL_CTX_check_private_key:檢查私鑰和證書文件
- SSL_new:方法一創建完成的上下文在通過此方法創建配套的SSL*
- SSL_set_fd:與上面創建的SSL和socket_fd綁定
- SSL_accept:服務端握手方法
- SSL_connect:客戶端握手方法
- SSL_write:消息發送,內部會對明文消息加密並調用socket發送
- SSL_read:消息接收,內部會從socket接收到密文數據再解碼成文明返回
- SSL_shutdown:通知對方關閉本次加密會話
- SSL_free:釋放SSL*
C++編寫socket利用openssl介面開發測試代碼
在熟悉以上基本概念之後,根據測試先行和敏捷開發的原則。我們接下來就要直接使用c++開發一個socket測試程式,並利用openssl介面進行加密通信。以下代碼的開發和運行系統為ubuntu 16.04 LTS,openssl版本為1.1.1d 10 Sep 2019,開發工具為Visual Studio Code 1.41.1。
服務端源碼 server.cpp
#include <iostream> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <cstring> #include <netinet/in.h> #include <string> #include "openssl/ssl.h" #include "openssl/err.h" using namespace std; // 前置申明 struct ssl_ctx_st *InitSSLServer(const char *crt_file, const char *key_file); int main(int argc, char *argv[]) { ssl_ctx_st *ssl_ctx = InitSSLServer("../server.crt", "../server.key"); // 引入之前生成好的私鑰文件和證書文件 int sock = socket(AF_INET, SOCK_STREAM, 0); sockaddr_in sin; memset(&sin, 0, sizeof(sin)); sin.sin_family = AF_INET; sin.sin_addr.s_addr = INADDR_ANY; sin.sin_port = htons(10020); // 指定通信埠 int res = ::bind(sock, (sockaddr *)&sin, sizeof(sin)); if (res == -1) { return -1; } listen(sock, 1); // 開始監聽 // 只接受一次客戶端的連接 int client_fd = accept(sock, 0, 0); cout << "Client accept success!" << endl; ssl_st *ssl = SSL_new(ssl_ctx); SSL_set_fd(ssl, client_fd); res = SSL_accept(ssl); // 執行SSL層握手 if (res != 1) { ERR_print_errors_fp(stderr); return -1; } // 握手完成,接受消息併發送一次應答 char buf[1024] = {0}; int len = SSL_read(ssl, buf, sizeof(buf)); cout << buf << endl; string s = "Hi Client, I'm CppSSLSocket Server."; SSL_write(ssl, s.c_str(), s.size()); // 釋放資源 SSL_free(ssl); SSL_CTX_free(ssl_ctx); return 0; } struct ssl_ctx_st *InitSSLServer(const char *crt_file, const char *key_file) { // 創建通信上下文 ssl_ctx_st *ssl_ctx = SSL_CTX_new(TLS_server_method()); if (!ssl_ctx) { cout << "ssl_ctx new failed" << endl; return nullptr; } int res = SSL_CTX_use_certificate_file(ssl_ctx, crt_file, SSL_FILETYPE_PEM); if (res != 1) { ERR_print_errors_fp(stderr); return nullptr; } res = SSL_CTX_use_PrivateKey_file(ssl_ctx, key_file, SSL_FILETYPE_PEM); if (res != 1) { ERR_print_errors_fp(stderr); return nullptr; } res = SSL_CTX_check_private_key(ssl_ctx); if (res != 1) { return nullptr; } return ssl_ctx; }
客戶端源碼 client.cpp
#include <iostream> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <cstring> #include <netinet/in.h> #include <arpa/inet.h> #include <string> #include "openssl/ssl.h" #include "openssl/err.h" using namespace std; struct ssl_ctx_st *InitSSLClient(); int main(int argc, char *argv[]) { int sock = socket(AF_INET, SOCK_STREAM, 0); sockaddr_in sin; memset(&sin, 0, sizeof(sin)); sin.sin_family = AF_INET; sin.sin_addr.s_addr = inet_addr("127.0.0.1"); sin.sin_port = htons(10020); // 首先執行socket連接 int res = connect(sock, (sockaddr *)&sin, sizeof(sin)); if (res != 0) { return -1; } cout << "Client connect success." << endl; ssl_ctx_st *ssl_ctx = InitSSLClient(); ssl_st *ssl = SSL_new(ssl_ctx); SSL_set_fd(ssl, sock); // 進行SSL層握手 res = SSL_connect(ssl); if (res != 1) { ERR_print_errors_fp(stderr); return -1; } string send_msg = "Hello Server, I'm CppSSLSocket Client."; SSL_write(ssl, send_msg.c_str(), send_msg.size()); char recv_msg[1024] = {0}; int recv_len = SSL_read(ssl, recv_msg, sizeof(recv_msg)); recv_msg[recv_len] = '\0'; cout << recv_msg << endl; SSL_shutdown(ssl); SSL_free(ssl); SSL_CTX_free(ssl_ctx); return 0; } struct ssl_ctx_st *InitSSLClient() { // 創建一個ssl客戶端的上下文 ssl_ctx_st *ssl_ctx = SSL_CTX_new(TLS_client_method()); return ssl_ctx; }
編譯使用Makefile,客戶端的修改TARGET即可
TARGET=server.x SRC=$(wildcard *.cpp) OBJS=$(patsubst %.cpp,%.o,$(SRC)) LIBS=-lssl -lcrypto $(TARGET):$(SRC) g++ -std=c++11 $^ -o $@ $(LIBS) clean: rm -fr $(TARGET) $(OBJS)
如果在服務端和客戶端都可以正常發送和接收顯示消息,即表示通信正常。
C++編寫openssl與libevent安全通信服務端
當前項目使用的libevent版本為2.1,在編譯的時候需要在目標機器上預先編譯好openssl。否則編譯時檢測不到,無法生成對應介面。有關libevent的基礎可以參考smss開源系列的前期文章,這裡不再贅述。考慮到同構系統的開發案例網上的資料相對豐富,同時筆者目前的工作大多為異構系統開發為主。因此這裡選擇使用C++作為服務端,Java和NodeJs為客戶端的方式。如果讀者有需要也可以給我留言,我會補充Java作為服務端C++作為客戶端的相關案例。
目前使用libevent和openssl作為通信框架,在追求性能優先的物聯網項目中應用廣泛,開發難度也相對較低。libevent也提供了專門調用openssl的介面,它可以幫助我們管理SSL對象,不過SSL_CTX的維護還需要我們自己實現。與直接使用libevent創建服務端相比最大的區別在於我們需要自己創建socket並同時交給event_base和SSL_CTX來使用。
服務端源碼 libevent_server.cpp
#include <iostream> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <netinet/in.h> #include <cstring> #include <string> #include "openssl/ssl.h" #include "event2/event.h" #include "event2/listener.h" #include "event2/bufferevent.h" #include "event2/bufferevent_ssl.h" using namespace std; // 設置x.509證書文件和私鑰文件 ssl_ctx_st *InitServer(const char *crt_file, const char *key_file); // 創建通信ssl ssl_st *NewSSL(ssl_ctx_st *ssl_ctx, int socket); // 服務端連接監聽回調函數 void EvconnlistenerCB(struct evconnlistener *listener, evutil_socket_t socket, struct sockaddr *addr, int socklen, void *ctx); // 消息讀、寫和事件回調 void ReadCB(struct bufferevent *bev, void *ctx); void WriteCB(struct bufferevent *bev, void *ctx); void EventCB(struct bufferevent *bev, short what, void *ctx); static bool isSsl = false; int main(int argc, char *argv[]) { if (argc == 2) { if (strcmp(argv[1], "SSL") == 0) { isSsl = true; } } // 創建event_base event_base *base = event_base_new(); if (!base) { cout << "event_base_new fail" << endl; return -1; } // 創建SSL_CTX通信上下文 ssl_ctx_st *ssl_ctx = InitServer("../server.crt", "../server.key"); // 創建socket sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(10020); evconnlistener *listener = evconnlistener_new_bind( base, EvconnlistenerCB, ssl_ctx, LEV_OPT_REUSEABLE | LEV_OPT_CLOSE_ON_FREE, 10, (sockaddr *)&addr, sizeof(addr)); // 阻塞當前線程執行事件迴圈 event_base_dispatch(base); // 釋放資源 SSL_CTX_free(ssl_ctx); event_base_free(base); return 0; } void EvconnlistenerCB(evconnlistener *listener, evutil_socket_t socket, struct sockaddr *addr, int socklen, void *ctx) { cout << "Server EvconnlistenerCB..." << endl; // 獲取當前的事件迴圈上下文 event_base *base = evconnlistener_get_base(listener); bufferevent *bev = nullptr; // 判斷當前是否啟用ssl通信模式 if (isSsl) { ssl_ctx_st *ssl_ctx = (ssl_ctx_st *)ctx; ssl_st *ssl = NewSSL(ssl_ctx, socket); // 創建bufferevent,當bufferevent關閉的時候,會同時釋放ssl資源 bev = bufferevent_openssl_socket_new(base, socket, ssl, BUFFEREVENT_SSL_ACCEPTING, BEV_OPT_CLOSE_ON_FREE); bufferevent_setcb(bev, ReadCB, WriteCB, EventCB, ssl); } else { bev = bufferevent_socket_new(base, socket, BEV_OPT_CLOSE_ON_FREE); bufferevent_setcb(bev, ReadCB, WriteCB, EventCB, base); } // 註冊事件類型 bufferevent_enable(bev, EV_READ | EV_WRITE); } /** * ssl上下文初始化 * 考慮測試簡潔的需要,這裡沒有做多餘判斷 */ ssl_ctx_st *InitServer(const char *crt_file, const char *key_file) { ssl_ctx_st *ssl_ctx = SSL_CTX_new(TLS_server_method()); SSL_CTX_use_certificate_file(ssl_ctx, crt_file, SSL_FILETYPE_PEM); SSL_CTX_use_PrivateKey_file(ssl_ctx, key_file, SSL_FILETYPE_PEM); SSL_CTX_check_private_key(ssl_ctx); return ssl_ctx; } /** * 創建ssl介面並且和socket綁定 */ ssl_st *NewSSL(ssl_ctx_st *ssl_ctx, int socket) { ssl_st *ssl = SSL_new(ssl_ctx); SSL_set_fd(ssl, socket); return ssl; } void ReadCB(bufferevent *bev, void *ctx) { char buf[1024] = {0}; int len = bufferevent_read(bev, buf, sizeof(buf) - 1); buf[len] = '\0'; cout << buf << endl; string msg = "hello client, I'm server.\n"; bufferevent_write(bev, msg.c_str(), msg.size()); bufferevent_write(bev, buf, len); } void WriteCB(bufferevent *bev, void *ctx) { } void EventCB(bufferevent *bev, short what, void *ctx) { cout << "EventCB: " << what << endl; if (what & BEV_EVENT_CONNECTED) { cout << "Event:BEV_EVENT_CONNECTED" << endl; } if (what & BEV_EVENT_ERROR && what & BEV_EVENT_READING) { cout << "Event:BEV_EVENT_READING" << endl; bufferevent_free(bev); } if (what & BEV_EVENT_ERROR && what & BEV_EVENT_WRITING) { cout << "Event:BEV_EVENT_WRITING" << endl; bufferevent_free(bev); } }
編譯用的Makefile文件
TARGET=server.x SRC=$(wildcard *.cpp) OBJS=$(patsubst %.cpp,%.o,$(SRC)) LIBS=-lssl -lcrypto -levent -levent_openssl $(TARGET):$(SRC) g++ -std=c++11 $^ -o $@ $(LIBS) clean: rm -fr $(TARGET) $(OBJS)
特別需要註意bufferevent_openssl_socket_new方法包含了對bufferevent和SSL的管理,因此當連接關閉的時候不再需要SSL_free。可執行文件server.x接收SSL作為參數,作為是否啟用安全通信的標識。
讀者可以使用上一節生成的client.x與本節的程式通信,方便測試結果。
三 *基於Node.js的(加密)通信測試
*註:如果您不熟悉electron可以跳過本節,不妨礙後面的學習
由於electron不是本文的重點,因此如何創建和開發electron項目做過過多介紹。本例使用electron-vue作為模板,使用vue-cli直接創建。我們將分別使用Node.js的net包和tls包創建通信客戶端。
net.Socket連接示例:
this.socket = net.connect(10020, "127.0.0.1", () => { console.log("socket 伺服器連接成功..."); this.socket.write("Hello Server, I'm Nodejs.", () => { console.log("發送完成~"); }); }); this.socket.on("data", data => { console.log(data.toString()); });
tls.connect連接示例:
this.socket = tls.connect( { host: "127.0.0.1", port: 10020, rejectUnauthorized: false }, () => { console.log("ssl 伺服器連接成功..."); this.socket.write("Hello Server, I'm Nodejs.", () => { console.log("發送完成~"); }); } ); this.socket.on("data", data => { console.log(data.toString()); });
由於之前我們通過openssl生成的x.509證書為自簽名證書,因此在使用tls.connect的時候需要指定rejectUnauthorized屬性。
讀者可以利用這套代碼和上一節創建的server.x分別進行普通通信和安全通信,以判斷功能是否正常。
四 創建基於SSLEngine的NIO通信
如果說之前的知識你都能夠掌握,那麼從這裡開始才是本文的重點,也是難點所在。網上對於SSLEngine的介紹資料相對較少,且大多都沒有經過完整測試,確實造成學習曲線過於陡峭。加之筆者認為Java對於SSLEngine的設計的確不太合理,因此強烈不建議讀者在實際項目中使用。事實上,SSL/TLS協議的握手過程非常複雜,涉及到加密和秘鑰交換等多個步驟。無論是基於C語言的openssl還是基於Node.js的tls.connect都將握手的過程封裝到內部。現在筆者將通過介紹SSLEngine讓你對這一過程有所瞭解。
ByteBuffer分析
io面向流(stream)開發,而nio面向緩衝(buffer)開發。很多人對此也不陌生,但是在工作中我發現能夠深入理解這句話的人比較少。什麼叫面向流(stream)?為什麼有區別於面向緩衝(buffer)?傳統io在向文件或資料庫請求數據的時候。由於需要請求操作系統資源,因此存在需要等待響應的過程。它不同於單純的代碼執行只需要使用cpu資源,io操作還需要涉及匯流排資源,磁碟資源等。在這個過程中,由於無法確定數據什麼時候會返回,只能做阻塞等待。nio的做法相當於告知操作系統:我已經在用戶態申請好了一塊記憶體空間(buffer),當內核接收到數據以後請直接寫到我的空間中。因此,使用nio編程的特點之一就是對數據的處理往往需要通過回調函數(callback)。作為最常用的緩衝對象——ByteBuffer,你有多熟悉?
ByteBuffer最重要的三個屬性:
- capacity 表示該緩衝區的最大容量,任何操作最大容量的讀寫操作都屬於非法
- limit 如果當前是寫入態,limit等於capacity。如果當前是讀取態,limit表示當前一共有多少有效數據。註意,寫入態和讀取態是我創造的名詞,buffer本身並不存在這兩個狀態
- position 當前數據區的讀/寫位置指針
當你開始往buffer中寫入數據的時候,pos會不斷增加,limit等於cap。寫入完成後,如果你想要讀取數據,第一步必須進行翻轉(flip)。翻轉以後的數據區pos為0,而limit則等於之前寫入的pos。如果在讀取數據的時候,無法一次性處理完。我們可以使用compact()方法將已經讀取的數據清除。
為了加深印象,請大家思考一個問題:如果我向一個ByteBuffer中寫入了數據,假設當前緩衝區的狀態為 java.nio.HeapByteBuffer[pos=1305 lim=16921 cap=16921]。我又讀取了94個位元組,當前緩衝區狀態為 java.nio.HeapByteBuffer[pos=94 lim=1305 cap=16921]。此時調用compact(),緩衝區的狀態是什麼情況?
根據jdk官方文檔上的解釋,compact()方法會將緩衝區中的數據按位複製,pos複製到0,pos + 1複製到1,以此類推,最後是將limit-1複製到limit-pos。事實上方法內部還幫我們做了一次翻轉操作,當前的緩衝區狀態為 java.nio.HeapByteBuffer[pos=1211 lim=16921 cap=16921]。
非阻塞SocketChannel
目前幾乎所有支持非阻塞的通信框架都基於React模式開發,通過在IO管道上註冊多個事件回調以達到非同步處理的效果。又因為回調的使用原來越多,因此Java 8也提出了函數式介面的概念,同時引入蘭姆達表達式以讓用戶能夠設計出更適合閱讀和維護的代碼。
NIO在socket上的運用Java提供了SocketChannel和Selector對象。
非阻塞客戶端 NioSocket.java
package socket; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.util.Set; public class NioSocket { /** * 連接方法 * * @param host 伺服器主機地址 * @param port 伺服器埠 */ public static void connection(String host, int port) throws IOException { Selector sel = Selector.open(); // 創建事件選擇器 InetSocketAddress addr = new InetSocketAddress(host, port); SocketChannel socket = SocketChannel.open(); // 創建非阻塞socket對象 socket.configureBlocking(false).register(sel, SelectionKey.OP_CONNECT | SelectionKey.OP_READ); // 配置非阻塞模式和向Selector註冊連接事件與數據可讀事件 socket.connect(addr); while (true) { // 等待間隔 if (sel.select(10) > 0) { Set<SelectionKey> keys = sel.selectedKeys(); for(SelectionKey key : keys) { keys.remove(key); // 移除事件並處理 if(key.isConnectable()) { socket.finishConnect(); String reqMsg = "Hello Server, I'm JavaClient."; ByteBuffer reqBuf = ByteBuffer.wrap(reqMsg.getBytes()); socket.write(reqBuf); } else if(key.isReadable()) { ByteBuffer respBuf = ByteBuffer.allocate(1024); int length = socket.read(respBuf); if(length > 0) { String respMsg = new String(respBuf.array(), 0, length); System.out.println(respMsg); } } } } } } public static void main(String[] args) { try { NioSocket.connection("127.0.0.1", 10020); } catch (IOException e) { e.printStackTrace(); } } }
當有註冊的事件產生的時候,我們能夠通過selectedKey()方法獲取完整的事件隊列。如果事件沒有被處理,會在下一次事件迴圈中重新觸發,因此處理完成的事件需要從隊列中刪除。
阻塞式加密通信 SSLSocket
接下來我們將難度升級,看一下利用SSLSocket如何開發加密通信的客戶端。Java為我們提供了javax.net.ssl包,裡面都是與SSL/TLS加密通信相關的組件。由於服務端使用的是自簽名證書,因此我們需要重寫TrustManager的實現
package tls; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import javax.net.ssl.X509TrustManager; public class X509SelfSignTrustManager implements X509TrustManager { @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { for (int i = 0; i < chain.length; i++) { System.out.println(chain[i]); } } @Override public X509Certificate[] getAcceptedIssuers() { return null; } }
作為客戶端checkClientTrusted()和getAcceptedIssuers()方法都不會被調用。checkServerTrusted()方法用來檢查服務端的證書,我們只將證書內容列印出來。
package tls; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; public class Ssl { public SSLSocket connection(String host, int port) throws Exception { SSLContext context = SSLContext.getInstance("SSL"); context.init(null, new TrustManager[] {new X509SelfSignTrustManager()} , new java.security.SecureRandom()); SSLSocketFactory factory = context.getSocketFactory(); return (SSLSocket) factory.createSocket(host, port); } public static void main(String[] args) { Ssl ssl = new Ssl(); SSLSocket sslSocket = null; try { sslSocket = ssl.connection("127.0.0.1", 10020); OutputStream output = sslSocket.getOutputStream(); String msg = "Hello Server, I'm BioSSLClient."; output.write(msg.getBytes()); output.flush(); InputStream input = sslSocket.getInputStream(); byte[] buf = new byte[1024]; int len = input.read(buf); String ss = new String(buf, 0, len); System.out.println(ss); } catch (Exception e) { e.printStackTrace(); } finally { try { sslSocket.close(); } catch (IOException e) { e.printStackTrace(); } } } }
首先是需要創建基於SSL協議的上下文對象SSLContext
使用我們自己實現的證書管理器進行初始化
創建SSLSocketFactory,並通過它實例化SSLSocket
通信過程基本就是操作io流,這裡不做贅述
SSLEngine——抽象化的握手和加/解密介面
先看一下規範的SSL/TLS握手步驟:
基本的通信大致可以分為4個過程:
- 選擇協議版本和會話ID
- 服務端發送證書和秘鑰交換數據
- 客戶端處理證書和生成秘鑰交換數據併發送給服務端
- 會話秘鑰協商成功,握手完成
因為SSLEngine僅僅是針對SSL層進行了抽象,因此底層通訊介面需要自己創建。因為打算使用nio,我將創建一個SocketChannel。
SSLEngine也通過SSLContext實例化,SSLContext還能夠實例化一個SSLSession對象,使用SSLSession幫助我們創建兩種緩存:應用數據緩存和網路數據緩存。顧名思義,應用數據緩存用來存儲明文數據,網路數據緩存代表將要發送或接收到的密文數據。它們通過SSLEngine的wrap()和unwrap()方法相互轉換。使用SSLEngine的難點是執行握手操作,關鍵點在於如何理解內部的兩個枚舉類型:
SSLEngineResult.HandshakeStatus:
- NEED_WRAP 當前有數據需要被加密併發送
- NEED_UNWRAP 當前有數據應該被讀取並解密
- NEED_TASK 需要執行運算任務
- FINISHED 握手完成
- NOT_HANDSHAKING 當前不處於握手狀態中
特別註意,FINISHED狀態只會在握手完成後的最後一步操作中出現,之後再獲取狀態都會顯示為NOT_HANDSHAKING(SSLEngine為什麼會這樣設計我也沒看懂)。我曾經以為NOT_HANDSHAKING狀態表示握手已斷開,一度很不理解。
SSLEngineResult.Status:在執行wrap()或unwrap()操作後
- OK 執行成功
- BUFFER_OVERFLOW 寫入緩存區不足,通常表示unwrap()的第二個參數設置的buffer剩餘空間不足
- BUFFER_UNDERFLOW 輸出緩衝區不足,通常表示wrap()的第一個參數設置的buffer中沒有數據
- CLOSED SSLEngine已經被關閉,無法執行任何方法
利用SSLEngine進行握手的時候,我們會多次使用wrap()和unwrap()方法。此時如果打開斷點你會發現明明沒有提供明文數據,經過wrap()後密文緩存中卻有數據。或者接收到密文數據後經過unwrap()方法,卻沒得到任何明文數據。原因是,握手階段的任何數據都在SSLEngine內部處理(這個設計很奇怪,不明白Java的設計者們如此設計的初衷是什麼)。
package tls; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.util.Iterator; import java.util.concurrent.TimeUnit; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLEngineResult; import javax.net.ssl.SSLEngineResult.HandshakeStatus; import javax.net.ssl.SSLEngineResult.Status; import javax.net.ssl.SSLSession; import javax.net.ssl.TrustManager; public class NioSsl { private SocketChannel sc; private SSLEngine sslEngine; private Selector selector; private HandshakeStatus hsStatus; private Status status; private ByteBuffer localNetData; private ByteBuffer localAppData; private ByteBuffer remoteNetData; private ByteBuffer remoteAppData; public void connection(String host, int port) throws Exception { SSLContext sslContext = SSLContext.getInstance("SSL"); sslContext.init(null, new TrustManager[] { new X509SelfSignTrustManager() }, new java.security.SecureRandom()); sslEngine = sslContext.createSSLEngine(); sslEngine.setUseClientMode(true); SSLSession session = sslEngine.getSession(); localAppData = ByteBuffer.allocate(session.getApplicationBufferSize()); localNetData = ByteBuffer.allocate(session.getPacketBufferSize()); remoteAppData = ByteBuffer.allocate(session.getApplicationBufferSize()); remoteNetData = ByteBuffer.allocate(session.getPacketBufferSize()); remoteNetData.clear(); SocketChannel channel = SocketChannel.open(); selector = Selector.open(); channel.configureBlocking(false).register(selector, SelectionKey.OP_CONNECT | SelectionKey.OP_READ | SelectionKey.OP_WRITE); InetSocketAddress addr = new InetSocketAddress(host, port); channel.connect(addr); sslEngine.beginHandshake(); hsStatus = sslEngine.getHandshakeStatus(); while (true) { if (selector.select(10) > 0) { Iterator<SelectionKey> it = selector.selectedKeys().iterator(); while (it.hasNext()) { SelectionKey selectionKey = it.next(); it.remove(); handleSocketEvent(selectionKey); } } } } private void handleSocketEvent(SelectionKey key) throws IOException, InterruptedException { if (key.isConnectable()) { System.out.println("isConnectable..."); sc = (SocketChannel) key.channel(); sc.finishConnect(); doHandshake(); localAppData.clear(); localAppData.put("Hello Server, I'm NioSslClient.".getBytes()); localAppData.flip(); localNetData.clear(); SSLEngineResult result = sslEngine.wrap(localAppData, localNetData); hsStatus = result.getHandshakeStatus(); status = result.getStatus(); if (status == Status.OK) { localNetData.flip(); while (localNetData.hasRemaining()) { sc.write(localNetData); } } } else if (key.isReadable()) { System.out.println("isReadable..."); sc = (SocketChannel) key.channel(); remoteNetData.clear(); remoteAppData.clear(); int len = sc.read(remoteNetData); System.out.println("接受服務端加密數據長度:" + len); remoteNetData.flip(); SSLEngineResult result = sslEngine.unwrap(remoteNetData, remoteAppData); hsStatus = result.getHandshakeStatus(); status = result.getStatus(); remoteAppData.flip(); byte[] buf = new byte[remoteAppData.limit()]; remoteAppData.get(buf); System.out.println(new String(buf)); } } private void doHandshake() throws IOException, InterruptedException { SSLEngineResult result; int count = 0; while (hsStatus != SSLEngineResult.HandshakeStatus.FINISHED) { TimeUnit.MILLISECONDS.sleep(100); switch (hsStatus) { case NEED_TASK: System.out.println("當前握手狀態:NEED_TASK"); Runnable runnable; while ((runnable = sslEngine.getDelegatedTask()) != null) { runnable.run(); } hsStatus = sslEngine.getHandshakeStatus(); break; case NEED_UNWRAP: System.out.println("當前握手狀態:NEED_UNWRAP"); count = sc.read(remoteNetData); System.out.println("獲取位元組數:" + count); remoteNetData.flip(); remoteAppData.clear(); do { result = sslEngine.unwrap(remoteNetData, remoteAppData); } while (result.getStatus() ==