開源項目SMSS發開指南(四)——SSL/TLS加密通訊詳解

本文將詳細介紹如何在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之前的版本可能與後面的代碼示例有所不一樣。html

electron桌面應用:electron是一套依賴google的V8引擎直接使用HTML/JS/CSS建立桌面應用的跨平臺解決方案。若是你須要開發輕量化的桌面端應用,electron基本是不二選擇。從我的的實踐來看,不管是開發生態仍是開發效率都強於Qt。使用electron能夠調用nodejs相關接口完成與系統的交互。vue

Java-nio開發包:基本是如今做爲Java中高級開發的必備技能。java

javax.net.ssl開發包:屬於Java對SSL/TLS支持的比較底層的開發包。目前在應用中更多會選擇Netty等集成式框架,若是你的項目中須要一些定製化功能能夠選擇它做爲支持。建議在項目中慎重使用。因爲一些特殊緣由,Java只提供了SSLSocket對象,底層只支持阻塞式訪問。文章最後會提供一個我我的實現的SSLSocketChannel對象,方便讀者在基礎上進行二次封裝。node

SSL/TLS通訊:安全通訊的目的是在原有的tcp/ip層和應用層之間增長了一個稱之爲SSL/TLS的加/解密層來實現的。在網絡協議層中的位置大體以下:ios

在OSI七層網絡協議的定義中,它處於表示層。程序開發的方式通常是在完成tcp/ip創建鏈接後,開始ssl/tls握手。發佈ssl的服務端須要具有一個私鑰文件(.key)以及與私鑰配套的證書文件(.crt)。證書包含了公鑰和對公鑰的簽名,還有一些用來證實源安全的信息。證書須要到專門的機構申請而且有年費要求,鑑於各位讀者僅用於自學,後面生成的證書咱們會作自簽名。ssl/tls握手的目的是在客戶端和服務端之間協商一個安全的對稱祕鑰,用來爲本次會話的消息加解密,因爲這對祕鑰僅通訊的服務端和客戶端持有,會話結束即消失。c++

二 libevent和openssl

生成x.509證書

首選在安裝好openssl的機器上建立私鑰文件:server.keyvue-cli

> openssl genrsa -out server.key 2048數據庫

獲得私鑰文件後咱們須要一個證書請求文件:server.csr,未來你能夠拿這個證書請求向正規的證書管理機構申請證書編程

> openssl req -new -key server.key -out server.csrubuntu

最後咱們生成自簽名的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直接通訊接口,下面逐一分析:

  1. SSL_CTX_new:新版本摒棄了一些老的接口,目前建議基本統一使用此方法來建立通訊上下文
  2. SSL_CTX_free:釋放SSL_CTX*
  3. SSL_CTX_use_certificate_file:設置證書文件
  4. SSL_CTX_use_PrivateKey_file:設置私鑰文件,與上面的證書文件必須配套不然檢測不經過
  5. SSL_CTX_check_private_key:檢查私鑰和證書文件
  6. SSL_new:方法一建立完成的上下文在經過此方法建立配套的SSL*
  7. SSL_set_fd:與上面建立的SSL和socket_fd綁定
  8. SSL_accept:服務端握手方法
  9. SSL_connect:客戶端握手方法
  10. SSL_write:消息發送,內部會對明文消息加密並調用socket發送
  11. SSL_read:消息接收,內部會從socket接收到密文數據再解碼成文明返回
  12. SSL_shutdown:通知對方關閉本次加密會話
  13. 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個過程:

  1. 選擇協議版本和會話ID
  2. 服務端發送證書和祕鑰交換數據
  3. 客戶端處理證書和生成祕鑰交換數據併發送給服務端
  4. 會話祕鑰協商成功,握手完成

由於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() == SSLEngineResult.Status.OK
                        && result.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_UNWRAP);


                hsStatus = result.getHandshakeStatus();
                status = result.getStatus();

                remoteNetData.compact();
                if (hsStatus == SSLEngineResult.HandshakeStatus.FINISHED) {
                    System.out.println("===========" + hsStatus + "===========");
                    
                }
                break;
            case NEED_WRAP:
                System.out.println("當前握手狀態:NEED_WRAP");
                localNetData.clear();
                result = sslEngine.wrap(ByteBuffer.allocate(0), localNetData);
                hsStatus = result.getHandshakeStatus();
                status = result.getStatus();
                if (status != Status.OK) {
                    throw new RuntimeException("status: " + status);
                }
                localNetData.flip();
                while (localNetData.hasRemaining()) {
                    int len = sc.write(localNetData);
                    System.out.println("發送字節數:" + len);
                }
                hsStatus = sslEngine.getHandshakeStatus();
                break;
            default:
                break;
            }
        }
        hsStatus = sslEngine.getHandshakeStatus();
        System.out.println("===========" + hsStatus + "===========");
    }

    public static void main(String[] args) {
        NioSsl nioSsl = new NioSsl();
        try {
            nioSsl.connection("127.0.0.1", 10020);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

源碼中我已經設置了睡眠時間和必要的消息輸出。讀者能夠複製到IDE中結合C++端的服務協同測試。若是通訊成功,你應該能夠在客戶端看到x.509證書打印和13次狀態改變。去除NEED_TASK狀態,再對比SSL/TLS協議的握手規範學習。

SSLSocketChannel 源碼

若是你可以順利看到這裏,那麼恭喜你。在這篇知識分享的文章中,你應該多少有些收穫。爲了準備這些東西,我用了幾乎整個2020年的春節假期(幸虧假期延長了,不然時間還不夠)。最後是我本身封裝的SSLSocketChannel,使用了函數式接口以及蘭姆達表達式。

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.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
import java.util.function.Supplier;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLEngineResult;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.SSLEngineResult.HandshakeStatus;

public class SSLSocketChannel {
    private volatile boolean isQuit = false;
    private SocketChannel socket = null;
    private Selector selector = null;
    private ExecutorService pool = null;
    private LinkedList<Function<byte[], byte[]>> readBufQueue = new LinkedList<>();
    private LinkedList<Supplier<byte[]>> writeBufQueue = new LinkedList<>();
    private Lock writeLock = new ReentrantLock();
    private Lock readLock = new ReentrantLock();
    private SSLEngine sslEngine;
    private HandshakeStatus hsStatus;

    private ByteBuffer localAppData, remoteAppData;
    private ByteBuffer localNetData, remoteNetData;

    public SSLSocketChannel() throws IOException {
        this.selector = Selector.open(); // 打開事件選擇器
        this.pool = Executors.newSingleThreadExecutor();
    }

    /**
     * 建立一個非堵塞的Socket並註冊鏈接事件和讀取事件
     * 
     * @param host
     * @param port
     * @throws IOException
     */
    public void connect(String host, int port) throws IOException {
        InetSocketAddress addr = new InetSocketAddress(host, port);
        socket = SocketChannel.open();
        socket.configureBlocking(false).register(selector, SelectionKey.OP_CONNECT | SelectionKey.OP_READ);
        socket.connect(addr);
    }

    /**
     * 網絡事件循環線程
     * 
     * @return 線程結束
     */
    public Future<Void> dispatch() {
        Future<Void> fut = this.pool.submit(() -> {
            while (!isQuit) {
                if (selector.select(10) > 0) {
                    Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
                    while (iter.hasNext()) {
                        SelectionKey key = iter.next();
                        if (key.isConnectable()) {
                            socket.finishConnect();
                            this.sslHandshake();
                        } else if (key.isReadable()) {
                            remoteNetData.clear();
                            int length = socket.read(remoteNetData);
                            if (length > 0) {
                                remoteNetData.flip();
                                remoteAppData.clear();
                                SSLEngineResult result = sslEngine.unwrap(remoteNetData, remoteAppData);
                                if (handleResult(result)) {
                                    remoteAppData.flip();
                                    byte[] b = new byte[remoteAppData.limit()];
                                    remoteAppData.get(b);
                                    try {
                                        readLock.lock();
                                        for (Function<byte[], byte[]> fn : readBufQueue) {
                                            byte[] r = fn.apply(b);
                                            if (r != null) {
                                                ByteBuffer buf = ByteBuffer.wrap(r);
                                                socket.write(buf);
                                            }
                                        }
                                    } finally {
                                        readLock.unlock();
                                    }
                                }
                            }
                        }
                        iter.remove();
                    }
                }
                if (socket.isConnected() && writeBufQueue.size() > 0
                        && (hsStatus == SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING
                                || hsStatus == SSLEngineResult.HandshakeStatus.FINISHED)) {
                    try {
                        writeLock.lock();
                        Supplier<byte[]> sup = null;
                        while ((sup = writeBufQueue.poll()) != null) {
                            localAppData.clear();
                            localAppData.put(sup.get());
                            localAppData.flip();
                            localNetData.clear();
                            SSLEngineResult result = sslEngine.wrap(localAppData, localNetData);
                            if (handleResult(result)) {
                                localNetData.flip();
                                while (localNetData.hasRemaining()) {
                                    socket.write(localNetData);
                                }
                            }
                        }
                    } finally {
                        writeLock.unlock();
                    }
                }
            }
            return null;
        });
        this.pool.shutdown();
        return fut;
    }

    /**
     * 添加數據進入發送隊列
     * 
     * @param 函數式接口
     * @see Supplier
     */
    public void write(Supplier<byte[]> s) {
        try {
            writeLock.lock();
            writeBufQueue.push(s);
        } finally {
            writeLock.unlock();
        }
    }

    /**
     * 添加接收器進入接收隊列
     * 
     * @param 函數式接口
     * @see Function
     */
    public void read(Function<byte[], byte[]> f) {
        try {
            readLock.lock();
            readBufQueue.push(f);
        } finally {
            readLock.unlock();
        }
    }

    /**
     * SSL/TLS 握手
     * 
     * @throws InterruptedException
     * @throws NoSuchAlgorithmException
     * @throws KeyManagementException
     * @throws IOException
     */
    public void sslHandshake()
            throws InterruptedException, NoSuchAlgorithmException, KeyManagementException, IOException {
        SSLContext sslContext = SSLContext.getInstance("SSL");
        sslContext.init(null, new TrustManager[] { new X509SelfSignTrustManager() }, new java.security.SecureRandom());
        sslEngine = sslContext.createSSLEngine();
        sslEngine.setUseClientMode(true);
        SSLSession sslSession = sslEngine.getSession();
        localAppData = ByteBuffer.allocate(sslSession.getApplicationBufferSize()); // 本地應用數據緩存
        localNetData = ByteBuffer.allocate(sslSession.getPacketBufferSize()); // 本地加密數據緩存
        remoteAppData = ByteBuffer.allocate(sslSession.getApplicationBufferSize()); // 遠端應用數據緩存
        remoteNetData = ByteBuffer.allocate(sslSession.getPacketBufferSize()); // 遠端加密數據緩存
        sslEngine.beginHandshake();
        hsStatus = sslEngine.getHandshakeStatus();
        SSLEngineResult result;
        // 循環判斷指導握手完成
        while (hsStatus != SSLEngineResult.HandshakeStatus.FINISHED) {
            switch (hsStatus) {
            case NEED_WRAP:
                localNetData.clear();
                result = sslEngine.wrap(ByteBuffer.allocate(0), localNetData); // 第一個參數設置空包,SSLEngine會將握手數據寫入網絡包
                hsStatus = result.getHandshakeStatus();
                if (handleResult(result)) {
                    localNetData.flip();
                    // 確保數據所有發送完成
                    while (localNetData.hasRemaining()) {
                        socket.write(localNetData);
                    }
                }
                break;
            case NEED_UNWRAP:
                int len = socket.read(remoteNetData); // 讀取網絡數據
                if (len == -1) {
                    break;
                }
                remoteNetData.flip();
                remoteAppData.clear();
                do {
                    result = sslEngine.unwrap(remoteNetData, remoteAppData); // 與握手相關的數據SSLEngine會自行處理,不會輸出至第二個參數
                    hsStatus = result.getHandshakeStatus();
                } while (handleResult(result) && hsStatus == SSLEngineResult.HandshakeStatus.NEED_UNWRAP);
                // 一次性沒有完成處理的數據經過壓縮的方式處理,等待下一次數據寫入
                remoteNetData.compact();
                break;
            case NEED_TASK:
                // SSLEngine後臺任務
                Runnable runnable;
                while ((runnable = sslEngine.getDelegatedTask()) != null) {
                    runnable.run();
                }
                hsStatus = sslEngine.getHandshakeStatus();
                break;
            default:
                break;
            }
        }
        // 握手完成將全部緩存清空
        localAppData.clear();
        localNetData.clear();
        remoteAppData.clear();
        remoteNetData.clear();
    }

    private boolean handleResult(SSLEngineResult result) {
        switch (result.getStatus()) {
        case OK:
            return true;
        case BUFFER_OVERFLOW:
            return false;
        case BUFFER_UNDERFLOW:
            return false;
        case CLOSED:
            return false;
        default:
            return false;
        }
    }

    public static void main(String[] args) {
        try {
            SSLSocketChannel sslSocketChannel = new SSLSocketChannel();
            sslSocketChannel.connect("127.0.0.1", 10020);
            sslSocketChannel.dispatch();
            sslSocketChannel.read((b) -> {
                String s = new String(b);
                System.out.println(s);
                return null;
            });
            sslSocketChannel.write(() -> {
                return "hello ssl".getBytes();
            });
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

 

相關文章:《開源項目SMSS開發指南》

相關文章
相關標籤/搜索