使用SSL進行網絡加密傳輸

一、概述

在 Acl 的網絡通信模塊中,爲了支持安全網絡傳輸,引入了第三方 SSL 庫,當前支持 Polarssl 及其升級版 MbedTLS,Acl 庫中通過抽象與封裝,大大簡化了 SSL 的使用過程(現在開源的 SSL 庫使用確實太複雜了),以下是在 Acl 庫中使用 SSL 的特點:

  • 爲了不給不使用 SSL 功能的用戶造成編譯上的障礙,Acl 庫採用動態加載 SSL 動態庫方式,這樣在連接時就不必提供 SSL 庫(當然,通過設置編譯開關,也允許用戶採用靜態連接 SSL 庫的方式);
  • 在 Acl 的工程中,僅包含了指定版本的 Polarssl/Mbedtls 頭文件(在 acl/include/ 目錄下),這些頭文件在編譯 Acl 的 SSL 模塊時會使用到,且不對外暴露,因此使用者需要自行提供對應版本的 SSL 動態二進制庫(SSL庫的源代碼可以去官方 https://tls.mbed.org/ 下載,或者去 https://github.com/acl-dev/third_party 處下載);
  • 在 Acl SSL 模塊中,分爲全局配置類和 IO 通信類,配置類對象只需在程序啓動時進行創建與初始化,且整個進程中按單例方式使用;IO 通信類對象與每一個 TCP 連接所對應的 socket 進行綁定,TCP 連接建立時進行初始化,進行 SSL 握手並接管 IO 過程;
  • Acl SSL 模塊支持服務端及客戶端方式,在服務端模塊時需要加載數字證書及證書私鑰;
  • Acl SSL 模塊支持阻塞與非阻塞兩種通信方式,阻塞方式還可以用在 Acl 協程通信中;
  • Acl SSL 模塊已經應用在 Acl HTTP 通信中,從而方便用戶編寫支持 HTTPS/Websocket 的客戶端或服務端程序;同時,Acl SSL 模塊也給 Acl Redis 模塊提供了安全通信功能;
  • Acl SSL 模塊是線程安全的,雖然官方提供的 Mbedtls 庫中增加支持線程安全的編譯選項,但其默認情況下卻是將此功能關閉的(這真是一個坑人的地方),當你需要打開線程支持功能時還必須得要提供線程鎖功能(通過函數回調註冊自己的線程鎖,好在 Acl 庫中有跨平臺的線程模塊),這應該是 Mbedtls 默認情況下不打開線程支持的原因;
  • 當你使用 Mbedtls 時,建議從 https://github.com/acl-dev/third_party/tree/master/mbedtls-2.7.12 下載 Mbedtls 源碼編譯,此處的 Mbedtls 與官方的主要區別是:
    1. 在 config.h 中打開了線程安全的編譯選項,同時添加了用於線程安全的互斥鎖頭文件:threading_alt.h;
    2. Mbedtls 庫編譯後生成了三個庫文件:libmbedcrypto/libmbedx509/libmbedtls,而原來 Polarssl 只生成一個庫文件,所以爲了用戶使用方便,修改了 libray/CMakeLists.txt 文件,可以將這三個庫文件合併成一個;
    3. 增加了 visualc/VC2012(而官方僅提供了 VS2010),這樣在 Windows 平臺下可以使用 VS 2012 來編譯生成 mbedtls 庫。

二、API 接口說明

爲了支持更加通用的 SSL 接口,在 Acl SSL 模塊中定義了兩個基礎類:sslbase_confsslbase_io,其中 ssbase_conf 類對象可以用做全局單一實例,ssbase_io 類對象用來給每一個 TCP socket 對象提供安全 IO 通信功能。

2.1、sslbase_conf 類

在 ssbase_conf 類中定義了純虛方法:open,用來創建 SSL IO 通信類對象,在當前所支持 Polarssl 和 MbedTSL 中的配置類中(分別爲:acl::polarssl_confacl::mbedtls_conf)均實現了該方法。下面是 open 方法的具體說明:

/**
 * 純虛方法,創建 SSL IO 對象
 * @param nblock {bool} 是否爲非阻塞模式
 * @return {sslbase_io*}
 */
virtual sslbase_io* open(bool nblock) = 0;

在客戶端或服務端創建 SSL IO 對象(即:sslbase_io 對象)時調用,被用來與 TCP socket 進行綁定。下面是綁定過程:

bool bind_ssl_io(acl::socket_stream& conn, acl::sslbase_conf& ssl_conf)
{
	// 創建一個阻塞式 SSL IO 對象
	bool non_block = false;
	acl::sslbase_io* ssl = ssl_conf.open(non_block); 
	
	// 將 SSL IO 對象與 TCP 連接流對象進行綁定,在綁定過程中會進行 SSL 握手,
	// 如果 SSL 握手失敗,則返回該 SSL IO 對象,返回 NULL 表示綁定成功。
	if (conn.setup_hook(ssl) == ssl) {
		return false;
	} else {
		return true;
	}
}

其中 acl::sslbase_io 的父類爲 acl::stream_hook,在acl::stream 流基礎類中提供了方法setup_hook用來註冊外部 IO 過程,其中的參數類型爲stream_hook ,通過綁定外部 IO 過程,將 SSL IO 過程與 acl 中的流處理 IO 過程進行綁定,從而使 acl 的 IO 流過程具備了 SSL 安全傳輸能力。

下面的幾個接口用在服務端進行證書及私鑰加載過程:

/**                                                                    
 * 添加一個服務端/客戶端自己的證書,可以多次調用本方法加載多個證書 
 * @param crt_file {const char*} 證書文件全路徑,非空 
 * @return {bool} 添加證書是否成功 
 */                                                                    
 virtual bool add_cert(const char* crt_file);
/**                                                                    
 * 添加服務端/客戶端的密鑰(每個配置實例只需調用一次本方法) 
 * @param key_file {const char*} 密鑰文件全路徑,非空
 * @param key_pass {const char*} 密鑰文件的密碼,沒有密鑰密碼可寫 NULL
 * @return {bool} 設置是否成功
 */
virtual bool set_key(const char* key_file, const char* key_pass = NULL)/**
 * 當爲服務端模式時是否啓用會話緩存功能,有助於提高 SSL 握手效率
 * @param on {bool} 是否在服務端啓用會話緩存方式
 * 注:該函數僅對服務端模式有效
 */
virtual void enable_cache(bool on)

2.2、sslbase_io 類

acl::sslbase_io 類對象與每一個 TCP 連接對象 acl::socket_stream 進行綁定,使 acl::socket_stream 具備了進行 SSL 安全傳輸的能力,在 acl::sslbase_io類中聲明瞭純虛方法handshake,這使之成爲純虛類;另外,acl::sslbase_io 雖然繼承於acl::stream_hook類,但並沒有實現 acl::stream_hook 中規定的四個純虛方法:openon_closereadsend,這幾個虛方法也需要 acl::sslbase_io的子類來實現,目前acl::sslbase_io有兩個子類acl::polarssl_ioacl::mbedtls_io 分別用來支持 Polarssl 及 MbedTLS。
下面是這幾個純虛方法的聲明:

/**
 * ssl 握手純虛方法(屬於 sslbase_io 類)
 * @return {bool} 返回 true 表示 SSL 握手成功,否則表示失敗
 */
virtual bool handshake(void) = 0;

下面幾個虛方法聲明於 acl::stream_hook 類中:

/**
 * 讀數據接口
 * @param buf {void*} 讀緩衝區地址,讀到的數據將存放在該緩衝區中
 * @param len {size_t} buf 緩衝區大小
 * @return {int} 讀到字節數,當返回值 < 0 時表示出錯
 */
virtual int read(void* buf, size_t len) = 0;

/**
 * 發送數據接口
 * @param buf {const void*} 發送緩衝區地址
 * @param len {size_t} buf 緩衝區中數據的長度(必須 > 0) 
 * @return {int} 寫入的數據長度,返回值 <0 時表示出錯
 */
virtual int send(const void* buf, size_t len) = 0;

/**
 * 在 stream/aio_stream 的 setup_hook 內部將會調用 stream_hook::open
 * 過程,以便於子類對象用來初始化一些數據及會話
 * @param s {ACL_VSTREAM*} 在 setup_hook 內部調用該方法將創建的流對象
 *  作爲參數傳入
 * @return {bool} 如果子類實例返回 false,則 setup_hook 調用失敗且會恢復原樣
 */
virtual bool open(ACL_VSTREAM* s) = 0; 

/**
 * 當 stream/aio_stream 流對象關閉前將會回調該函數以便於子類實例做一些善後工作
 * @param alive {bool} 該連接是否依然正常
 * @return {bool}
 */
virtual bool on_close(bool alive) { (void) alive; return true; } 

/**
 * 當 stream/aio_stream 對象需要釋放 stream_hook 子類對象時調用此方法
 */
virtual void destroy(void) {}

以上幾個虛方法均可以在 acl::polarssl_ioacl::mbedtls_io 中看到被實現。

三、編程示例

2.1、服務器模式(使用 MbedTLS)

首先給出一個完整的支持 SSL 的服務端例子,該例子使用了 MbedTLS 做爲 SSL 庫,如果想切換成 Polarssl 也非常簡單,方法類似(該示例位置:https://github.com/acl-dev/acl/tree/master/lib_acl_cpp/samples/ssl/server):

#include <assert.h>
#include "lib_acl.h"
#include "acl_cpp/lib_acl.hpp"

class echo_thread : public acl::thread {
public:
        echo_thread(acl::sslbase_conf& ssl_conf, acl::socket_stream* conn)
        : ssl_conf_(ssl_conf), conn_(conn) {}

private:
        acl::sslbase_conf&  ssl_conf_;
        acl::socket_stream* conn_;

        ~echo_thread(void) { delete conn_; }

        // @override
        void* run(void) {
                conn_->set_rw_timeout(60);

                // 給 socket 安裝 SSL IO 過程
                if (!setup_ssl()) {
                        return NULL;
                }

                do_echo();

                delete this;
                return NULL;
        }

        bool setup_ssl(void) {
                bool non_block = false;
                acl::sslbase_io* ssl = ssl_conf_.open(non_block);

                // 對於使用 SSL 方式的流對象,需要將 SSL IO 流對象註冊至網絡
                // 連接流對象中,即用 ssl io 替換 stream 中默認的底層 IO 過程
                if (conn_->setup_hook(ssl) == ssl) {
                        printf("setup ssl IO hook error!\r\n");
                        ssl->destroy();
                        return false;
                }
                return true;
        }

        void do_echo(void) {
                char buf[4096];

                while (true) {
                        int ret = conn_->read(buf, sizeof(buf), false);
                        if (ret == -1) {
                                break;
                        }
                        if (conn_->write(buf, ret) == -1) {
                                break;
                        }
                }
        }
};

static void start_server(const acl::string addr, acl::sslbase_conf& ssl_conf) {
        acl::server_socket ss;
        if (!ss.open(addr)) {
                printf("listen %s error %s\r\n", addr.c_str(), acl::last_serror());
                return;
        }

        while (true) {
                acl::socket_stream* conn = ss.accept();
                if (conn == NULL) {
                        printf("accept error %s\r\n", acl::last_serror());
                        break;
                }
                acl::thread* thr = new echo_thread(ssl_conf, conn);
                thr->set_detachable(true);
                thr->start();
        }
}

static bool ssl_init(const acl::string& ssl_crt, const acl::string& ssl_key,
        acl::mbedtls_conf& ssl_conf) {

        ssl_conf.enable_cache(true);

        // 加載 SSL 證書
        if (!ssl_conf.add_cert(ssl_crt)) {
                printf("add ssl crt=%s error\r\n", ssl_crt.c_str());
                return false;
        }

        // 設置 SSL 證書私鑰
        if (!ssl_conf.set_key(ssl_key)) {
                printf("set ssl key=%s error\r\n", ssl_key.c_str());
                return false;
        }

        return true;
}

static void usage(const char* procname) {
        printf("usage: %s -h [help]\r\n"
                " -s listen_addr\r\n"
                " -L ssl_libs_path\r\n"
                " -c ssl_crt\r\n"
                " -k ssl_key\r\n", procname);
}

int main(int argc, char* argv[]) {
        acl::string addr = "0.0.0.0|2443";
#if defined(__APPLE__)
        acl::string ssl_lib = "../libmbedtls_all.dylib";
#elif defined(__linux__)
        acl::string ssl_lib = "../libmbedtls_all.so";
#elif defined(_WIN32) || defined(_WIN64)
        acl::string ssl_path = "../mbedtls.dll";

        acl::acl_cpp_init();
#else
# error "unknown OS type"
#endif
        acl::string ssl_crt = "../ssl_crt.pem", ssl_key = "../ssl_key.pem";

        int ch;
        while ((ch = getopt(argc, argv, "hs:L:c:k:")) > 0) {
                switch (ch) {
                case 'h':
                        usage(argv[0]);
                        return 0;
                case 's':
                        addr = optarg;
                        break;
                case 'L':
                        ssl_lib = optarg;
                        break;
                case 'c':
                        ssl_crt = optarg;
                        break;
                case 'k':
                        ssl_key = optarg;
                        break;
                default:
                        break;
                }
        }

        acl::log::stdout_open(true);
        
        // 設置 MbedTLS 動態庫路徑
        const std::vector<acl::string>& libs = ssl_lib.split2(",; \t");
        if (libs.size() == 1) {
                acl::mbedtls_conf::set_libpath(libs[0]);
        } else if (libs.size() == 3) {
                // libcrypto, libx509, libssl);
                acl::mbedtls_conf::set_libpath(libs[0], libs[1], libs[2]);
        } else {
                printf("invalid ssl_lib=%s\r\n", ssl_lib.c_str());
                return 1;
        }

        // 加載 MbedTLS 動態庫
        if (!acl::mbedtls_conf::load()) {
                printf("load %s error\r\n", ssl_lib.c_str());
                return 1;
        }

        // 初始化服務端模式下的全局 SSL 配置對象
        bool server_side = true;

        // SSL 證書校驗級別
        acl::mbedtls_verify_t verify_mode = acl::MBEDTLS_VERIFY_NONE;

        acl::mbedtls_conf ssl_conf(server_side, verify_mode);

        if (!ssl_init(ssl_crt, ssl_key, ssl_conf)) {
                printf("ssl_init failed\r\n");
                return 1;
        }
        
        start_server(addr, ssl_conf);
        return 0;
}

關於該示例有以下幾點說明:

  • 該服務端例子使用了 MbedTLS 庫;
  • 採用了動態加載 MbedTLS 動態庫的方式;
  • 動態加載時需要設置 MbedTLS 動態庫的路徑,然後再加載,但在設置動態庫的路徑時卻有兩種方式,之所以有兩種設置 MbedTLS 動態庫路徑的方法,主要是因爲原來的 Polarssl 只生成一個庫,而到 MbedTLS 後卻生成了三個庫:libmbedcrypto,libmbedx509 和 libmbedtls,其中的依賴關係是 libmbedx509 依賴於 libmbedcrypto,libmbedtls 依賴於 libmbedx509 和 libmbedcrypto;但在 Windows 平臺上,官方卻只提供了生成一個庫(將這三個庫合併)的工程;因此,在 acl::mbedtls_conf 中在加載動態庫時,提供兩種方式,一個接口是用來設置三個庫的位置並加載,另一個接口用來設置一個統一庫的位置度加載;
  • 該例子大體處理流程:
    • 通過 acl::mbedtls_conf::set_libpath 方法設置 MbedTLS 的三個動態庫或一個統一的動態庫,然後調用 acl::mbedtls_conf::load 加載動態庫;
    • ssl_init 函數中,調用基類 acl::sslbase_conf 中的虛方法 add_certset_key 分別用來加載 SSL 數字證書及證書私鑰;
    • start_server 函數中,監聽本地服務地址,每接收一個 TCP 連接(對應一個 acl::socket_stream 對象)便啓動一個線程進行 echo 過程;
    • 在客戶端處理線程中,調用 echo_thread::setup_ssl 方法給該 acl::socket_stream TCP 流對象綁定一個 SSL IO 對象,即:先通過調用 acl::mbedtls_conf::open 方法創建一個 acl::mbedtls_io SSL IO 對象,然後通過 acl::socket_stream 的基類中的方法 set_hook 將該 SSL IO 對象與 TCP 流對象進行綁定並完成 SSL 握手過程;
    • SSL 握手成功後進入到 echo_thread::do_echo 函數中進行簡單的 SSL 安全 echo 過程。

2.2、客戶端模式(使用 MbedTLS)

在熟悉了上面的 SSL 服務端編程後,下面給出使用 SSL 進行客戶端編程的示例(該示例位置:https://github.com/acl-dev/acl/tree/master/lib_acl_cpp/samples/ssl/client):

#include <assert.h>
#include "lib_acl.h"
#include "acl_cpp/lib_acl.hpp"

class echo_thread : public acl::thread {
public:
        echo_thread(acl::sslbase_conf& ssl_conf, const char* addr, int count)
        : ssl_conf_(ssl_conf), addr_(addr), count_(count) {}

        ~echo_thread(void) {}

private:
        acl::sslbase_conf&  ssl_conf_;
        acl::string addr_;
        int count_;

private:
        // @override
        void* run(void) {
                acl::socket_stream conn;
                conn.set_rw_timeout(60);
                if (!conn.open(addr_, 10, 10)) {
                        printf("connect %s error %s\r\n",
                                addr_.c_str(), acl::last_serror());
                        return NULL;
                }

                // 給 socket 安裝 SSL IO 過程
                if (!setup_ssl(conn)) {
                        return NULL;
                }

                do_echo(conn);

                return NULL;
        }
        bool setup_ssl(acl::socket_stream& conn) {
                bool non_block = false;
                acl::sslbase_io* ssl = ssl_conf_.open(non_block);

                // 對於使用 SSL 方式的流對象,需要將 SSL IO 流對象註冊至網絡
                // 連接流對象中,即用 ssl io 替換 stream 中默認的底層 IO 過程
                if (conn.setup_hook(ssl) == ssl) {
                        printf("setup ssl IO hook error!\r\n");
                        ssl->destroy();
                        return false;
                }
                printf("ssl setup ok!\r\n");

                return true;
        }

        void do_echo(acl::socket_stream& conn) {
                const char* data = "hello world!\r\n";
                int i;
                for (i = 0; i < count_; i++) {
                        if (conn.write(data, strlen(data)) == -1) {
                                break;
                        }

                        char buf[4096];
                        int ret = conn.read(buf, sizeof(buf) - 1, false);
                        if (ret == -1) {
                                printf("read over, count=%d\r\n", i + 1);
                                break;
                        }
                        buf[ret] = 0;
                        if (i == 0) {
                                printf("read: %s", buf);
                        }
                }
                printf("thread-%lu: count=%d\n", acl::thread::self(), i);
        }
};

static void start_clients(acl::sslbase_conf& ssl_conf, const acl::string addr,
        int cocurrent, int count) {

        std::vector<acl::thread*> threads;
        for (int i = 0; i < cocurrent; i++) {
                acl::thread* thr = new echo_thread(ssl_conf, addr, count);
                threads.push_back(thr);
                thr->start();
        }

        for (std::vector<acl::thread*>::iterator it = threads.begin();
                it != threads.end(); ++it) {
                (*it)->wait(NULL);
                delete *it;
        }
}

static void usage(const char* procname) {
        printf("usage: %s -h [help]\r\n"
                " -s listen_addr\r\n"
                " -L ssl_libs_path\r\n"
                " -c cocurrent\r\n"
                " -n count\r\n", procname);
}

int main(int argc, char* argv[]) {
        acl::string addr = "0.0.0.0|2443";
#if defined(__APPLE__)
        acl::string ssl_lib = "../libmbedtls_all.dylib";
#elif defined(__linux__)
        acl::string ssl_lib = "../libmbedtls_all.so";
#elif defined(_WIN32) || defined(_WIN64)
        acl::string ssl_path = "../mbedtls.dll";

        acl::acl_cpp_init();
#else
# error "unknown OS type"
#endif

        int ch, cocurrent = 10, count = 10;
        while ((ch = getopt(argc, argv, "hs:L:c:n:")) > 0) {
                switch (ch) {
                case 'h':
                        usage(argv[0]);
                        return 0;
                case 's':
                        addr = optarg;
                        break;
                case 'L':
                        ssl_lib = optarg;
                        break;
                case 'c':
                        cocurrent = atoi(optarg);
                        break;
                case 'n':
                        count = atoi(optarg);
                        break;
                default:
                        break;
                }
        }

        acl::log::stdout_open(true);

        // 設置 MbedTLS 動態庫路徑
        const std::vector<acl::string>& libs = ssl_lib.split2(",; \t");
        if (libs.size() == 1) {
                acl::mbedtls_conf::set_libpath(libs[0]);
        } else if (libs.size() == 3) {
                // libcrypto, libx509, libssl);
                acl::mbedtls_conf::set_libpath(libs[0], libs[1], libs[2]);
        } else {
                printf("invalid ssl_lib=%s\r\n", ssl_lib.c_str());
                return 1;
        }

        // 加載 MbedTLS 動態庫
        if (!acl::mbedtls_conf::load()) {
                printf("load %s error\r\n", ssl_lib.c_str());
                return 1;
        }

        // 初始化客戶端模式下的全局 SSL 配置對象
        bool server_side = false;

        // SSL 證書校驗級別
        acl::mbedtls_verify_t verify_mode = acl::MBEDTLS_VERIFY_NONE;
        acl::mbedtls_conf ssl_conf(server_side, verify_mode);
        start_clients(ssl_conf, addr, cocurrent, count);
        return 0;
}

在客戶方式下使用 SSL 時的方法與服務端時相似,不同之處是在客戶端下使用 SSL 時不必加載證書和設置私鑰。

2.3、非阻塞模式

在使用 SSL 進行非阻塞編程時,動態庫的加載、證書的加載及設置私鑰過程與阻塞式 SSL 編程方法相同,不同之處在於創建 SSL IO 對象時需要設置爲非阻塞方式,另外在 SSL 握手階段需要不斷檢測 SSL 握手是否成功,下面只給出相關不同之處,完整示例可以參考:https://github.com/acl-dev/acl/tree/master/lib_acl_cpp/samples/ssl/aio_server,https://github.com/acl-dev/acl/tree/master/lib_acl_cpp/samples/ssl/aio_client):

  • 調用 acl::sslbase_conf 中的虛方法 open 時傳入的參數爲 true 表明所創建的 SSL IO 對象爲非阻塞方式;
  • 在創建非阻塞 IO 對象後,需要調用 acl::aio_socket_stream 中的 read_wait 方法,以便可以觸發 acl::aio_istream::read_wakeup 回調,從而在該回調裏完成 SSL 握手過程;
  • 在非阻塞IO的讀回調裏需要調用 acl::sslbase_io 中的虛方法 handshake 嘗試進行 SSL 握手並通過 handshake_ok 檢測握手是否成功。
    下面給出在 read_wakeup 回調裏進行 SSL 握手的過程:
bool read_wakeup()
{
        acl::sslbase_io* hook = (acl::sslbase_io*) client_->get_hook();
        if (hook == NULL) { 
                // 非 SSL 模式,異步讀取數據
                //client_->read(__timeout);
                client_->gets(__timeout, false); 
                return true;
        }       

        // 嘗試進行 SSL 握手
        if (!hook->handshake()) {
                printf("ssl handshake failed\r\n");
                return false;
        }       

        // 如果 SSL 握手已經成功,則開始按行讀數據
        if (hook->handshake_ok()) {
                // 由 reactor 模式轉爲 proactor 模式,從而取消
                // read_wakeup 回調過程
                client_->disable_read();

                // 異步讀取數據,將會回調 read_callback
                //client_->read(__timeout);
                client_->gets(__timeout, false); 
                return true;
        }       

        // SSL 握手還未完成,等待本函數再次被觸發
        return true;
}

在該代碼片斷中,如果 SSL 握手一直處於進行中,則 read_wakeup 可能會被調用多次,這就意味着 handshake 握手過程也會被調用多次,然後再通過 handshake_ok 判斷握手是否已經成功,如果成果,則通過調用 gets 方法切換到 IO 過程(該 IO 過程對應的回調爲 read_callback),否則進行 SSL 握手過程(繼續等待 read_wakeup 被回調)。

發佈了125 篇原創文章 · 獲贊 8 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章