Swoole 源碼分析——Server模塊之OpenSSL(下)

前言

上一篇文章咱們講了 OpenSSL 的原理,接下來,咱們來講說如何利用 openssl 第三方庫進行開發,來爲 tcp 層進行 SSL 隧道加密php

OpenSSL 初始化

swoole 中,若是想要進行 ssl 加密,只須要以下設置便可:react

$serv = new swoole_server("0.0.0.0", 443, SWOOLE_PROCESS, SWOOLE_SOCK_TCP | SWOOLE_SSL);
$key_dir = dirname(dirname(__DIR__)).'/tests/ssl';

$serv->set(array(
    'worker_num' => 4,
    'ssl_cert_file' => $key_dir.'/ssl.crt',
    'ssl_key_file' => $key_dir.'/ssl.key',
));

_construct 構造函數

咱們先看看在構造函數中 SWOOLE_SSL 起了什麼做用:web

REGISTER_LONG_CONSTANT("SWOOLE_SSL", SW_SOCK_SSL, CONST_CS | CONST_PERSISTENT);

PHP_METHOD(swoole_server, __construct)
{
    char *serv_host;
    long serv_port = 0;
    long sock_type = SW_SOCK_TCP;
    long serv_mode = SW_MODE_PROCESS;
    
    ...
    
    
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s|lll", &serv_host, &host_len, &serv_port, &serv_mode, &sock_type) == FAILURE)
    {
        swoole_php_fatal_error(E_ERROR, "invalid swoole_server parameters.");
        return;
    }
    
    ...

    swListenPort *port = swServer_add_port(serv, sock_type, serv_host, serv_port);
    
    ....
}


#define SW_SSL_CIPHER_LIST               "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH"
#define SW_SSL_ECDH_CURVE                "secp384r1"

swListenPort* swServer_add_port(swServer *serv, int type, char *host, int port)
{
    ...
    
    swListenPort *ls = SwooleG.memory_pool->alloc(SwooleG.memory_pool, sizeof(swListenPort));
    
    ...
    
    if (type & SW_SOCK_SSL)
    {
        type = type & (~SW_SOCK_SSL);
        if (swSocket_is_stream(type))
        {
            ls->type = type;
            ls->ssl = 1;
// #ifdef SW_USE_OPENSSL
            ls->ssl_config.prefer_server_ciphers = 1;
            ls->ssl_config.session_tickets = 0;
            ls->ssl_config.stapling = 1;
            ls->ssl_config.stapling_verify = 1;
            ls->ssl_config.ciphers = sw_strdup(SW_SSL_CIPHER_LIST);
            ls->ssl_config.ecdh_curve = sw_strdup(SW_SSL_ECDH_CURVE);
#endif
        }
    }
    
    ...
}

咱們能夠看到,初始化過程當中,會將常量 SWOOLE_SSL 轉化爲 SW_SOCK_SSL。而後調用 swServer_add_port 函數,在該函數中會設定不少用於 SSL 的參數。redis

  • prefer_server_ciphers 加密套件偏向於服務端而不是客戶端,也就是說會從服務端的加密套件從頭至尾依次查找最合適的,而不是從客戶端提供的列表尋找。
  • session_tickets 初始化,因爲 SSL 握手的非對稱運算不管是 RSA 仍是 ECDHE,都會消耗性能,故爲了提升性能,對於以前已經進行過握手的 SSL 鏈接,儘量減小握手 round time trip 以及運算。 SSL 提供 2 中不一樣的會話複用機制:算法

    (1) session id 會話複用。

    對於已經創建的 SSL 會話,使用 session idkeysession id 來自第一次請求的 server hello 中的 session id 字段),主密鑰爲 value 組成一對鍵值,保存在本地,服務器和客戶端都保存一份。緩存

    當第二次握手時,客戶端若想使用會話複用,則發起的 client hellosession id 會置上對應的值,服務器收到這個 client hello,解析 session id,查找本地是否有該 session id,若是有,判斷當前的加密套件和上個會話的加密套件是否一致,一致則容許使用會話複用,因而本身的 server hellosession id 也置上和 client hello 中同樣的值。而後計算對稱祕鑰,解析後續的操做。服務器

    若是服務器未查到客戶端的 session id 指定的會話(多是會話已經老化),則會從新握手,session id 要麼從新計算(和 client hellosession id 不同),要麼置成 0,這兩個方式都會告訴客戶端此次會話不進行會話複用。swoole

    (2) session ticket 會話複用session

    Session id會話複用有2個缺點,其一就是服務器會大量堆積會話,特別是在實際使用時,會話老化時間配置爲數小時,這種狀況對服務器內存佔用很是高。數據結構

    其次,若是服務器是集羣模式搭建,那麼客戶端和A各自保存的會話,在合B嘗試會話複用時會失敗(固然,你想用redis搭個集羣存session id也行,就是太麻煩)。

    Session ticket的工做流程以下:

    1:客戶端發起client hello,拓展中帶上空的session ticket TLS,代表本身支持session ticket。

    2:服務器在握手過程當中,若是支持session ticket,則發送New session ticket類型的握手報文,其中包含了可以恢復包括主密鑰在內的會話信息,固然,最簡單的就是隻發送master key。爲了讓中間人不可見,這個session ticket部分會進行編碼、加密等操做。

    3:客戶端收到這個session ticket,就把當前的master key和這個ticket組成一對鍵值保存起來。服務器無需保存任何會話信息,客戶端也無需知道session ticket具體表示什麼。

    4:當客戶端嘗試會話複用時,會在client hello的拓展中加上session ticket,而後服務器收到session ticket,回去進行解密、解碼能相關操做,來恢復會話信息。若是可以恢復會話信息,那麼久提取會話信息的主密鑰進行後續的操做。

  • staplingstapling_verify:

    OCSPOnline Certificate Status Protocol,在線證書狀態協議)是用來檢驗證書合法性的在線查詢服務,通常由證書所屬 CA 提供。

    假如服務端的私鑰被泄漏,對應的證書就會被加入黑名單,爲了驗證服務端的證書是否在黑名單中,某些客戶端會在 TLS 握手階段進一步協商時,實時查詢 OCSP 接口,並在得到結果前阻塞後續流程。OCSP 查詢本質是一次完整的 HTTP 請求 - 響應,這中間 DNS 查詢、創建 TCP、服務端處理等環節均可能耗費很長時間,致使最終創建 TLS 鏈接時間變得更長。

    OCSP StaplingOCSP 封套),是指服務端主動獲取 OCSP 查詢結果並隨着證書一塊兒發送給客戶端,從而讓客戶端跳過本身去驗證的過程,提升 TLS 握手效率。

  • ciphers 祕鑰套件:默認的加密套件是 "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH",關於加密套件咱們在上一章已經講解完畢
  • ecdh_curve: 是 ECDH 算法所須要的橢圓加密參數。

到這裏,SSL 的初始化已經完成。

Set 設置 SSL 參數

PHP_METHOD(swoole_server, set)
{
    zval *zset = NULL;
    
    ...
    
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &zset) == FAILURE)
    {
        return;
    }
    
    ...

    sw_zend_call_method_with_1_params(&port_object, swoole_server_port_class_entry_ptr, NULL, "set", &retval, zset);
}

static PHP_METHOD(swoole_server_port, set)
{
    ...
    
    if (port->ssl)
    {
        if (php_swoole_array_get_value(vht, "ssl_cert_file", v))
        {
            convert_to_string(v);
            if (access(Z_STRVAL_P(v), R_OK) < 0)
            {
                swoole_php_fatal_error(E_ERROR, "ssl cert file[%s] not found.", Z_STRVAL_P(v));
                return;
            }
            if (port->ssl_option.cert_file)
            {
                sw_free(port->ssl_option.cert_file);
            }
            port->ssl_option.cert_file = sw_strdup(Z_STRVAL_P(v));
            port->open_ssl_encrypt = 1;
        }
        if (php_swoole_array_get_value(vht, "ssl_key_file", v))
        {
            convert_to_string(v);
            if (access(Z_STRVAL_P(v), R_OK) < 0)
            {
                swoole_php_fatal_error(E_ERROR, "ssl key file[%s] not found.", Z_STRVAL_P(v));
                return;
            }
            if (port->ssl_option.key_file)
            {
                sw_free(port->ssl_option.key_file);
            }
            port->ssl_option.key_file = sw_strdup(Z_STRVAL_P(v));
        }
        if (php_swoole_array_get_value(vht, "ssl_method", v))
        {
            convert_to_long(v);
            port->ssl_option.method = (int) Z_LVAL_P(v);
        }
        //verify client cert
        if (php_swoole_array_get_value(vht, "ssl_client_cert_file", v))
        {
            convert_to_string(v);
            if (access(Z_STRVAL_P(v), R_OK) < 0)
            {
                swoole_php_fatal_error(E_ERROR, "ssl cert file[%s] not found.", port->ssl_option.cert_file);
                return;
            }
            if (port->ssl_option.client_cert_file)
            {
                sw_free(port->ssl_option.client_cert_file);
            }
            port->ssl_option.client_cert_file = sw_strdup(Z_STRVAL_P(v));
        }
        if (php_swoole_array_get_value(vht, "ssl_verify_depth", v))
        {
            convert_to_long(v);
            port->ssl_option.verify_depth = (int) Z_LVAL_P(v);
        }
        if (php_swoole_array_get_value(vht, "ssl_prefer_server_ciphers", v))
        {
            convert_to_boolean(v);
            port->ssl_config.prefer_server_ciphers = Z_BVAL_P(v);
        }

        if (php_swoole_array_get_value(vht, "ssl_ciphers", v))
        {
            convert_to_string(v);
            if (port->ssl_config.ciphers)
            {
                sw_free(port->ssl_config.ciphers);
            }
            port->ssl_config.ciphers = sw_strdup(Z_STRVAL_P(v));
        }
        if (php_swoole_array_get_value(vht, "ssl_ecdh_curve", v))
        {
            convert_to_string(v);
            if (port->ssl_config.ecdh_curve)
            {
                sw_free(port->ssl_config.ecdh_curve);
            }
            port->ssl_config.ecdh_curve = sw_strdup(Z_STRVAL_P(v));
        }
        if (php_swoole_array_get_value(vht, "ssl_dhparam", v))
        {
            convert_to_string(v);
            if (port->ssl_config.dhparam)
            {
                sw_free(port->ssl_config.dhparam);
            }
            port->ssl_config.dhparam = sw_strdup(Z_STRVAL_P(v));
        }

        if (swPort_enable_ssl_encrypt(port) < 0)
        {
            swoole_php_fatal_error(E_ERROR, "swPort_enable_ssl_encrypt() failed.");
            RETURN_FALSE;
        }
    }
    
    ...


}

這些 SSL 參數都是能夠自定義設置的,上面代碼最關鍵的是 swPort_enable_ssl_encrypt 函數,該函數調用了 openssl 第三方庫進行 ssl 上下文的初始化:

int swPort_enable_ssl_encrypt(swListenPort *ls)
{
    if (ls->ssl_option.cert_file == NULL || ls->ssl_option.key_file == NULL)
    {
        swWarn("SSL error, require ssl_cert_file and ssl_key_file.");
        return SW_ERR;
    }
    ls->ssl_context = swSSL_get_context(&ls->ssl_option);
    if (ls->ssl_context == NULL)
    {
        swWarn("swSSL_get_context() error.");
        return SW_ERR;
    }
    if (ls->ssl_option.client_cert_file
            && swSSL_set_client_certificate(ls->ssl_context, ls->ssl_option.client_cert_file,
                    ls->ssl_option.verify_depth) == SW_ERR)
    {
        swWarn("swSSL_set_client_certificate() error.");
        return SW_ERR;
    }
    if (ls->open_http_protocol)
    {
        ls->ssl_config.http = 1;
    }
    if (ls->open_http2_protocol)
    {
        ls->ssl_config.http_v2 = 1;
        swSSL_server_http_advise(ls->ssl_context, &ls->ssl_config);
    }
    if (swSSL_server_set_cipher(ls->ssl_context, &ls->ssl_config) < 0)
    {
        swWarn("swSSL_server_set_cipher() error.");
        return SW_ERR;
    }
    return SW_OK;
}

swSSL_get_context

能夠看到,上面最關鍵的函數就是 swSSL_get_context 函數,該函數初始化 SSL 並構建上下文環境的步驟爲:

  • OpenSSL 版本大於 1.1.0 後,SSL 簡化了初始化過程,只須要調用 OPENSSL_init_ssl 函數便可,在此以前必須手動調用 SSL_library_init(openssl 初始化)、SSL_load_error_strings(加載錯誤常量)、OpenSSL_add_all_algorithms (加載算法)
  • 利用 swSSL_get_method 函數選擇不一樣版本的 SSL_METHOD
  • 利用 SSL_CTX_new 函數建立上下文
  • 爲服務器配置參數,關於這些參數能夠參考官方文檔:List of SSL OP Flags,其中不少配置對於最新版原本說,沒有任何影響,僅僅做爲兼容舊版本而保留。
  • SSLKEY 文件通常都是由對稱加密算法所加密,這時候就須要調用 SSL_CTX_set_default_passwd_cbSSL_CTX_set_default_passwd_cb_userdata,不然在啓動 swoole 的時候,就須要手動在命令行中輸入該密碼。
  • 接着就須要將私鑰文件和證書文件的路徑傳入 SSL,相應的函數是 SSL_CTX_use_certificate_fileSSL_CTX_use_certificate_chain_fileSSL_CTX_use_PrivateKey_file,而後利用 SSL_CTX_check_private_key 來驗證私鑰。
void swSSL_init(void)
{
    if (openssl_init)
    {
        return;
    }
#if OPENSSL_VERSION_NUMBER >= 0x10100003L && !defined(LIBRESSL_VERSION_NUMBER)
    OPENSSL_init_ssl(OPENSSL_INIT_LOAD_CONFIG, NULL);
#else
    OPENSSL_config(NULL);
    SSL_library_init();
    SSL_load_error_strings();
    OpenSSL_add_all_algorithms();
#endif
    openssl_init = 1;
}

SSL_CTX* swSSL_get_context(swSSL_option *option)
{
    if (!openssl_init)
    {
        swSSL_init();
    }

    SSL_CTX *ssl_context = SSL_CTX_new(swSSL_get_method(option->method));
    if (ssl_context == NULL)
    {
        ERR_print_errors_fp(stderr);
        return NULL;
    }

    SSL_CTX_set_options(ssl_context, SSL_OP_SSLREF2_REUSE_CERT_TYPE_BUG);
    SSL_CTX_set_options(ssl_context, SSL_OP_MICROSOFT_BIG_SSLV3_BUFFER);
    SSL_CTX_set_options(ssl_context, SSL_OP_MSIE_SSLV2_RSA_PADDING);
    SSL_CTX_set_options(ssl_context, SSL_OP_SSLEAY_080_CLIENT_DH_BUG);
    SSL_CTX_set_options(ssl_context, SSL_OP_TLS_D5_BUG);
    SSL_CTX_set_options(ssl_context, SSL_OP_TLS_BLOCK_PADDING_BUG);
    SSL_CTX_set_options(ssl_context, SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS);
    SSL_CTX_set_options(ssl_context, SSL_OP_SINGLE_DH_USE);

    if (option->passphrase)
    {
        SSL_CTX_set_default_passwd_cb_userdata(ssl_context, option);
        SSL_CTX_set_default_passwd_cb(ssl_context, swSSL_passwd_callback);
    }

    if (option->cert_file)
    {
        /*
         * set the local certificate from CertFile
         */
        if (SSL_CTX_use_certificate_file(ssl_context, option->cert_file, SSL_FILETYPE_PEM) <= 0)
        {
            ERR_print_errors_fp(stderr);
            return NULL;
        }
        /*
         * if the crt file have many certificate entry ,means certificate chain
         * we need call this function
         */
        if (SSL_CTX_use_certificate_chain_file(ssl_context, option->cert_file) <= 0)
        {
            ERR_print_errors_fp(stderr);
            return NULL;
        }
        /*
         * set the private key from KeyFile (may be the same as CertFile)
         */
        if (SSL_CTX_use_PrivateKey_file(ssl_context, option->key_file, SSL_FILETYPE_PEM) <= 0)
        {
            ERR_print_errors_fp(stderr);
            return NULL;
        }
        /*
         * verify private key
         */
        if (!SSL_CTX_check_private_key(ssl_context))
        {
            swWarn("Private key does not match the public certificate");
            return NULL;
        }
    }

    return ssl_context;
}

static int swSSL_passwd_callback(char *buf, int num, int verify, void *data)
{
    swSSL_option *option = (swSSL_option *) data;
    if (option->passphrase)
    {
        size_t len = strlen(option->passphrase);
        if (len < num - 1)
        {
            memcpy(buf, option->passphrase, len + 1);
            return (int) len;
        }
    }
    return 0;
}

swSSL_get_method

咱們來看看如何利用不一樣版本的 OpenSSL 選取不一樣的 SSL_METHODswoole 默認使用 SW_SSLv23_METHOD,該方法支持 SSLv2SSLv3:

static const SSL_METHOD *swSSL_get_method(int method)
{
    switch (method)
    {
#ifndef OPENSSL_NO_SSL3_METHOD
    case SW_SSLv3_METHOD:
        return SSLv3_method();
    case SW_SSLv3_SERVER_METHOD:
        return SSLv3_server_method();
    case SW_SSLv3_CLIENT_METHOD:
        return SSLv3_client_method();
#endif
    case SW_SSLv23_SERVER_METHOD:
        return SSLv23_server_method();
    case SW_SSLv23_CLIENT_METHOD:
        return SSLv23_client_method();
/**
 * openssl 1.1.0
 */
#if OPENSSL_VERSION_NUMBER < 0x10100000L
    case SW_TLSv1_METHOD:
        return TLSv1_method();
    case SW_TLSv1_SERVER_METHOD:
        return TLSv1_server_method();
    case SW_TLSv1_CLIENT_METHOD:
        return TLSv1_client_method();
#ifdef TLS1_1_VERSION
    case SW_TLSv1_1_METHOD:
        return TLSv1_1_method();
    case SW_TLSv1_1_SERVER_METHOD:
        return TLSv1_1_server_method();
    case SW_TLSv1_1_CLIENT_METHOD:
        return TLSv1_1_client_method();
#endif
#ifdef TLS1_2_VERSION
    case SW_TLSv1_2_METHOD:
        return TLSv1_2_method();
    case SW_TLSv1_2_SERVER_METHOD:
        return TLSv1_2_server_method();
    case SW_TLSv1_2_CLIENT_METHOD:
        return TLSv1_2_client_method();
#endif
    case SW_DTLSv1_METHOD:
        return DTLSv1_method();
    case SW_DTLSv1_SERVER_METHOD:
        return DTLSv1_server_method();
    case SW_DTLSv1_CLIENT_METHOD:
        return DTLSv1_client_method();
#endif
    case SW_SSLv23_METHOD:
    default:
        return SSLv23_method();
    }
    return SSLv23_method();
}

雙向驗證

swSSL_get_context 函數以後,若是使用了雙向驗證,那麼還須要

  • 利用 SSL_CTX_set_verify 函數與 SSL_VERIFY_PEER 參數要求客戶端發送證書來進行雙向驗證
  • SSL_CTX_set_verify_depth 函數用於設置證書鏈的個數,證書鏈不能多於該參數
  • SSL_CTX_load_verify_locations 用於加載可信任的 CA 證書,注意這個並非客戶端用於驗證的證書,而是用來設定服務端 可信任CA 機構
  • SSL_load_client_CA_fileSSL_CTX_set_client_CA_list 用於設置服務端可信任的 CA 證書的列表,在握手過程當中將會發送給客戶端。:
int swSSL_set_client_certificate(SSL_CTX *ctx, char *cert_file, int depth)
{
    STACK_OF(X509_NAME) *list;

    SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, swSSL_verify_callback);
    SSL_CTX_set_verify_depth(ctx, depth);

    if (SSL_CTX_load_verify_locations(ctx, cert_file, NULL) == 0)
    {
        swWarn("SSL_CTX_load_verify_locations(\"%s\") failed.", cert_file);
        return SW_ERR;
    }

    ERR_clear_error();
    list = SSL_load_client_CA_file(cert_file);
    if (list == NULL)
    {
        swWarn("SSL_load_client_CA_file(\"%s\") failed.", cert_file);
        return SW_ERR;
    }

    ERR_clear_error();
    SSL_CTX_set_client_CA_list(ctx, list);

    return SW_OK;
}

NPN/ALPN 協議支持

若是使用了 http2 協議,還要調用 swSSL_server_http_advise 函數:

  • NPNALPN 都是爲了支持 HTTP/2 而開發的 TLS 擴展,1.0.2 版本以後纔開始支持 ALPN。當客戶端進行 SSL 握手的時候,客戶端和服務端之間會利用 NPN 協議或者 ALPN 來協商接下來到底使用 http/1.1 仍是 http/2
  • 二者的區別:

    • NPN 是服務端發送所支持的 HTTP 協議列表,由客戶端選擇;而 ALPN 是客戶端發送所支持的 HTTP 協議列表,由服務端選擇;
    • NPN 的協商結果是在 Change Cipher Spec 以後加密發送給服務端;而 ALPN 的協商結果是經過 Server Hello 明文發給客戶端;
  • 若是 openssl 僅僅支持 NPN 的時候,調用 SSL_CTX_set_next_protos_advertised_cb,不然調用 SSL_CTX_set_alpn_select_cb
  • SSL_CTX_set_next_protos_advertised_cb 函數中註冊了 swSSL_npn_advertised 函數,該函數返回了 SW_SSL_HTTP2_NPN_ADVERTISE SW_SSL_NPN_ADVERTISE
  • SSL_CTX_set_alpn_select_cb 函數中註冊了 swSSL_alpn_advertised 函數,該函數會繼續調用 SSL_select_next_proto 來和客戶端進行協商。
void swSSL_server_http_advise(SSL_CTX* ssl_context, swSSL_config *cfg)
{
#ifdef TLSEXT_TYPE_application_layer_protocol_negotiation
    SSL_CTX_set_alpn_select_cb(ssl_context, swSSL_alpn_advertised, cfg);
#endif

#ifdef TLSEXT_TYPE_next_proto_neg
    SSL_CTX_set_next_protos_advertised_cb(ssl_context, swSSL_npn_advertised, cfg);
#endif

    if (cfg->http)
    {
        SSL_CTX_set_session_id_context(ssl_context, (const unsigned char *) "HTTP", strlen("HTTP"));
        SSL_CTX_set_session_cache_mode(ssl_context, SSL_SESS_CACHE_SERVER);
        SSL_CTX_sess_set_cache_size(ssl_context, 1);
    }
}

#define SW_SSL_NPN_ADVERTISE             "\x08http/1.1"
#define SW_SSL_HTTP2_NPN_ADVERTISE       "\x02h2"

#ifdef TLSEXT_TYPE_application_layer_protocol_negotiation

static int swSSL_alpn_advertised(SSL *ssl, const uchar **out, uchar *outlen, const uchar *in, uint32_t inlen, void *arg)
{
    unsigned int srvlen;
    unsigned char *srv;

#ifdef SW_USE_HTTP2
    swSSL_config *cfg = arg;
    if (cfg->http_v2)
    {
        srv = (unsigned char *) SW_SSL_HTTP2_NPN_ADVERTISE SW_SSL_NPN_ADVERTISE;
        srvlen = sizeof (SW_SSL_HTTP2_NPN_ADVERTISE SW_SSL_NPN_ADVERTISE) - 1;
    }
    else
#endif
    {
        srv = (unsigned char *) SW_SSL_NPN_ADVERTISE;
        srvlen = sizeof (SW_SSL_NPN_ADVERTISE) - 1;
    }
    if (SSL_select_next_proto((unsigned char **) out, outlen, srv, srvlen, in, inlen) != OPENSSL_NPN_NEGOTIATED)
    {
        return SSL_TLSEXT_ERR_NOACK;
    }
    return SSL_TLSEXT_ERR_OK;
}
#endif

#ifdef TLSEXT_TYPE_next_proto_neg

static int swSSL_npn_advertised(SSL *ssl, const uchar **out, uint32_t *outlen, void *arg)
{
#ifdef SW_USE_HTTP2
    swSSL_config *cfg = arg;
    if (cfg->http_v2)
    {
        *out = (uchar *) SW_SSL_HTTP2_NPN_ADVERTISE SW_SSL_NPN_ADVERTISE;
        *outlen = sizeof (SW_SSL_HTTP2_NPN_ADVERTISE SW_SSL_NPN_ADVERTISE) - 1;
    }
    else
#endif
    {
        *out = (uchar *) SW_SSL_NPN_ADVERTISE;
        *outlen = sizeof(SW_SSL_NPN_ADVERTISE) - 1;
    }
    return SSL_TLSEXT_ERR_OK;
}
#endif

session 會話重用

全部的 session 必須都要有 session ID 上下文。對於服務端來講,session 緩存默認是不能使用的,能夠經過調用 SSL_CTX_set_session_id_context 函數來進行設置生效。產生 session ID 上下文的目的是保證重用的 session 的使用目的與 session 建立時的使用目的是一致的。好比,在 SSL web 服務器中產生的 session 不能自動地在 SSL FTP 服務中使用。於此同時,咱們可使用 session ID 上下文來實現對咱們的應用的更加細粒度的控制。好比,認證後的客戶端應該與沒有進行認證的客戶端有着不一樣的 session ID 上下文。上下文的內容咱們能夠任意選擇。正是經過函數 SSL_CTX_set_session_id_context 函數來設置上下文的,上下文的數據時第二個參數,第三個參數是數據的長度。

在設置了 session ID 上下文後,服務端就開啓了 session緩存;可是咱們的配置尚未完成。Session 有一個限定的生存期。在 OpenSSL 中的默認值是 300 秒。若是咱們須要改變這個生存期,使用函數 SSL_CTX_set_timeout。儘管服務端默認地會自動地清除過時的 session,咱們仍然能夠手動地調用SSL_CTX_flush_sessions 來進行清理。好比,當咱們關閉自動清理過時 session 的時候,就須要手動進行了。

一個很重要的函數:SSL_CTX_set_session_cache_mode,它容許咱們改變對相關緩存的行爲。與 OpenSSL 中其它的模式設置函數同樣,模式使用一些標誌的邏輯或來進行設置。其中一個標誌是 SSL_SESS_CACHE_NO_AUTO_CLEAR,它關閉自動清理過時 session 的功能。這樣有利於服務端更加高效嚴謹地進行處理,由於默認的行爲可能會有意想不到的延遲;

SSL_CTX_set_session_id_context(ssl_context, (const unsigned char *) "HTTP", strlen("HTTP"));
SSL_CTX_set_session_cache_mode(ssl_context, SSL_SESS_CACHE_SERVER);
SSL_CTX_sess_set_cache_size(ssl_context, 1);

加密套件的使用

加密套件的使用主要是使用 SSL_CTX_set_cipher_list 函數,此外若是須要 RSA 算法,還須要 SSL_CTX_set_tmp_rsa_callback 函數註冊 RSA 祕鑰的生成回調函數 swSSL_rsa_key_callback

在回調函數 swSSL_rsa_key_callback 中,首先申請一個大數數據結構 BN_new,而後將其設定爲 RSA_F4,該值表示公鑰指數 e,而後利用 RSA_generate_key_ex 函數生成祕鑰。RSAPublicKey_dup 函數和 RSAPrivateKey_dup 函數能夠提取公鑰與私鑰。

int swSSL_server_set_cipher(SSL_CTX* ssl_context, swSSL_config *cfg)
{
#ifndef TLS1_2_VERSION
    return SW_OK;
#endif
    SSL_CTX_set_read_ahead(ssl_context, 1);

    if (strlen(cfg->ciphers) > 0)
    {
        if (SSL_CTX_set_cipher_list(ssl_context, cfg->ciphers) == 0)
        {
            swWarn("SSL_CTX_set_cipher_list(\"%s\") failed", cfg->ciphers);
            return SW_ERR;
        }
        if (cfg->prefer_server_ciphers)
        {
            SSL_CTX_set_options(ssl_context, SSL_OP_CIPHER_SERVER_PREFERENCE);
        }
    }

#ifndef OPENSSL_NO_RSA
    SSL_CTX_set_tmp_rsa_callback(ssl_context, swSSL_rsa_key_callback);
#endif

    if (cfg->dhparam && strlen(cfg->dhparam) > 0)
    {
        swSSL_set_dhparam(ssl_context, cfg->dhparam);
    }
#if OPENSSL_VERSION_NUMBER < 0x10100000L
    else
    {
        swSSL_set_default_dhparam(ssl_context);
    }
#endif
    if (cfg->ecdh_curve && strlen(cfg->ecdh_curve) > 0)
    {
        swSSL_set_ecdh_curve(ssl_context);
    }
    return SW_OK;
}

#ifndef OPENSSL_NO_RSA
static RSA* swSSL_rsa_key_callback(SSL *ssl, int is_export, int key_length)
{
    static RSA *rsa_tmp = NULL;
    if (rsa_tmp)
    {
        return rsa_tmp;
    }

    BIGNUM *bn = BN_new();
    if (bn == NULL)
    {
        swWarn("allocation error generating RSA key.");
        return NULL;
    }

    if (!BN_set_word(bn, RSA_F4) || ((rsa_tmp = RSA_new()) == NULL)
            || !RSA_generate_key_ex(rsa_tmp, key_length, bn, NULL))
    {
        if (rsa_tmp)
        {
            RSA_free(rsa_tmp);
        }
        rsa_tmp = NULL;
    }
    BN_free(bn);
    return rsa_tmp;
}
#endif

到此,ssl 的上下文終於設置完畢,set 函數配置完成。

OpenSSL 端口的監聽與接收

當監聽的端口被觸發鏈接後,reactor 事件會調用 swServer_master_onAccept 函數,進而調用 accept 函數,創建新的鏈接,生成新的文件描述符 new_fd

此時須要調用 swSSL_create 函數將新的鏈接與 SSL 綁定。

swSSL_create 函數中,SSL_new 函數根據 ssl_context 建立新的 SSL 對象,利用 SSL_set_fd 綁定 SSLSSL_set_accept_state 函數對 SSL 進行鏈接初始化。

int swServer_master_onAccept(swReactor *reactor, swEvent *event)
{
    ...
    
    new_fd = accept(event->fd, (struct sockaddr *) &client_addr, &client_addrlen);
    
    ...
    
    swConnection *conn = swServer_connection_new(serv, listen_host, new_fd, event->fd, reactor_id);
    
    ...

    if (listen_host->ssl)
        {
            if (swSSL_create(conn, listen_host->ssl_context, 0) < 0)
            {
                bzero(conn, sizeof(swConnection));
                close(new_fd);
                return SW_OK;
            }
        }
        else
        {
            conn->ssl = NULL;
        }
    ...
}

int swSSL_create(swConnection *conn, SSL_CTX* ssl_context, int flags)
{
    SSL *ssl = SSL_new(ssl_context);
    if (ssl == NULL)
    {
        swWarn("SSL_new() failed.");
        return SW_ERR;
    }
    if (!SSL_set_fd(ssl, conn->fd))
    {
        long err = ERR_get_error();
        swWarn("SSL_set_fd() failed. Error: %s[%ld]", ERR_reason_error_string(err), err);
        return SW_ERR;
    }
    if (flags & SW_SSL_CLIENT)
    {
        SSL_set_connect_state(ssl);
    }
    else
    {
        SSL_set_accept_state(ssl);
    }
    conn->ssl = ssl;
    conn->ssl_state = 0;
    return SW_OK;
}

OpenSSL 套接字可寫

套接字寫就緒有如下幾種狀況:

  • 套接字在創建鏈接以後,只設置了監聽寫就緒,這時對於 OpenSSL 來講不須要任何處理,轉爲監聽讀就緒便可。
static int swReactorThread_onWrite(swReactor *reactor, swEvent *ev)
{
    ...
    
    if (conn->connect_notify)
    {
        conn->connect_notify = 0;
        
        if (conn->ssl)
        {
            goto listen_read_event;
        }
        
        ...
        
        listen_read_event:
        
        return reactor->set(reactor, fd, SW_EVENT_TCP | SW_EVENT_READ);
    }
    else if (conn->close_notify)
    {
        if (conn->ssl && conn->ssl_state != SW_SSL_STATE_READY)
        {
            return swReactorThread_close(reactor, fd);
        }
    
    }
    
    ...
    
    _pop_chunk: while (!swBuffer_empty(conn->out_buffer))
    {
        ...
        
        ret = swConnection_buffer_send(conn);
        
        ...
    
    }
}
  • 套接字可寫入數據時,會調用 swConnection_buffer_send 寫入數據,進而調用 swSSL_sendSSL_writeSSL_write 發生錯誤以後,函數會返回 SSL_ERROR_WANT_READSSL_ERROR_WANT_WRITE 等函數,這時須要將 errno 設置爲 EAGAIN,再次調用便可。
int swConnection_buffer_send(swConnection *conn)
{
    ...
    
    ret = swConnection_send(conn, chunk->store.ptr + chunk->offset, sendn, 0);
    
    ...

}

static sw_inline ssize_t swConnection_send(swConnection *conn, void *__buf, size_t __n, int __flags)
{
    ...
    
    _send:
    if (conn->ssl)
    {
        retval = swSSL_send(conn, __buf, __n);
    }
    
    if (retval < 0 && errno == EINTR)
    {
        goto _send;
    }
    else
    {
        goto _return;
    }

    _return:
    
    return retval;
    
    ...
}

ssize_t swSSL_send(swConnection *conn, void *__buf, size_t __n)
{
    int n = SSL_write(conn->ssl, __buf, __n);
    if (n < 0)
    {
        int _errno = SSL_get_error(conn->ssl, n);
        switch (_errno)
        {
        case SSL_ERROR_WANT_READ:
            conn->ssl_want_read = 1;
            errno = EAGAIN;
            return SW_ERR;

        case SSL_ERROR_WANT_WRITE:
            conn->ssl_want_write = 1;
            errno = EAGAIN;
            return SW_ERR;

        case SSL_ERROR_SYSCALL:
            return SW_ERR;

        case SSL_ERROR_SSL:
            swSSL_connection_error(conn);
            errno = SW_ERROR_SSL_BAD_CLIENT;
            return SW_ERR;

        default:
            break;
        }
    }
    return n;
}
  • 套接字已關閉。這時調用 swReactorThread_close,進而調用 swSSL_close

    在該函數中,首先要利用 SSL_in_init 來判斷當前 SSL 是否處於初始化握手階段,若是初始化還未完成,不能調用 shutdown 函數,應該使用 SSL_free 來銷燬 SSL 通道。

    在調用 SSL_shutdown 關閉通道以前,還須要調用 SSL_set_quiet_shutdown 設置靜默關閉選項,此時關閉通道並不會通知對端鏈接已經關閉。並利用 SSL_set_shutdown 關閉讀和寫。

    若是返回的數據並非 1,說明關閉通道的時候發生了錯誤。

int swReactorThread_close(swReactor *reactor, int fd)
{
    ...
    
    if (conn->ssl)
    {
        swSSL_close(conn);
    }
    
    ...

}

void swSSL_close(swConnection *conn)
{
    int n, sslerr, err;

    if (SSL_in_init(conn->ssl))
    {
        /*
         * OpenSSL 1.0.2f complains if SSL_shutdown() is called during
         * an SSL handshake, while previous versions always return 0.
         * Avoid calling SSL_shutdown() if handshake wasn't completed.
         */
        SSL_free(conn->ssl);
        conn->ssl = NULL;
        return;
    }

    SSL_set_quiet_shutdown(conn->ssl, 1);
    SSL_set_shutdown(conn->ssl, SSL_RECEIVED_SHUTDOWN | SSL_SENT_SHUTDOWN);

    n = SSL_shutdown(conn->ssl);

    swTrace("SSL_shutdown: %d", n);

    sslerr = 0;

    /* before 0.9.8m SSL_shutdown() returned 0 instead of -1 on errors */
    if (n != 1 && ERR_peek_error())
    {
        sslerr = SSL_get_error(conn->ssl, n);
        swTrace("SSL_get_error: %d", sslerr);
    }

    if (!(n == 1 || sslerr == 0 || sslerr == SSL_ERROR_ZERO_RETURN))
    {
        err = (sslerr == SSL_ERROR_SYSCALL) ? errno : 0;
        swWarn("SSL_shutdown() failed. Error: %d:%d.", sslerr, err);
    }

    SSL_free(conn->ssl);
    conn->ssl = NULL;
}

OpenSSL 讀就緒

OpenSSL 讀就緒的時候也是有如下幾個狀況:

  • 鏈接剛剛創建,由 swReactorThread_onWrite 轉調過來。此時須要驗證 SSL 當前狀態。
static int swReactorThread_onRead(swReactor *reactor, swEvent *event)
{
    if (swReactorThread_verify_ssl_state(reactor, port, event->socket) < 0)
    {
        return swReactorThread_close(reactor, event->fd);
        
        ...
        
        return port->onRead(reactor, port, event);
    }
}
  • swReactorThread_verify_ssl_state 函數用於驗證 SSL 當前的狀態,若是當前狀態僅僅是套接字綁定,尚未進行握手(conn->ssl_state == 0),那麼就要調用 swSSL_accept 函數進行握手,握手以後 conn->ssl_state = SW_SSL_STATE_READY
  • 握手以後有三種狀況,一是握手成功,此時設置 ssl_state 狀態,低版本 ssl 設定 SSL3_FLAGS_NO_RENEGOTIATE_CIPHERS 標誌,禁用會話重協商,而後返回 SW_READY;二是握手暫時不可用,須要返回 SW_WAIT,等待下次讀就緒再次握手;三是握手失敗,返回 SW_ERROR,調用 swReactorThread_close 關閉套接字。
  • 握手成功以後,要向 worker 進程發送鏈接成功的任務,進而調用 onConnection 回調函數。
static sw_inline int swReactorThread_verify_ssl_state(swReactor *reactor, swListenPort *port, swConnection *conn)
{
    swServer *serv = reactor->ptr;
    if (conn->ssl_state == 0 && conn->ssl)
    {
        int ret = swSSL_accept(conn);
        if (ret == SW_READY)
        {
            if (port->ssl_option.client_cert_file)
            {
                swDispatchData task;
                ret = swSSL_get_client_certificate(conn->ssl, task.data.data, sizeof(task.data.data));
                if (ret < 0)
                {
                    goto no_client_cert;
                }
                else
                {
                    swFactory *factory = &SwooleG.serv->factory;
                    task.target_worker_id = -1;
                    task.data.info.fd = conn->fd;
                    task.data.info.type = SW_EVENT_CONNECT;
                    task.data.info.from_id = conn->from_id;
                    task.data.info.len = ret;
                    factory->dispatch(factory, &task);
                    goto delay_receive;
                }
            }
            no_client_cert:
            if (SwooleG.serv->onConnect)
            {
                swServer_tcp_notify(SwooleG.serv, conn, SW_EVENT_CONNECT);
            }
            delay_receive:
            if (serv->enable_delay_receive)
            {
                conn->listen_wait = 1;
                return reactor->del(reactor, conn->fd);
            }
            return SW_OK;
        }
        else if (ret == SW_WAIT)
        {
            return SW_OK;
        }
        else
        {
            return SW_ERR;
        }
    }
    return SW_OK;
}

int swSSL_accept(swConnection *conn)
{
    int n = SSL_do_handshake(conn->ssl);
    /**
     * The TLS/SSL handshake was successfully completed
     */
    if (n == 1)
    {
        conn->ssl_state = SW_SSL_STATE_READY;
#if OPENSSL_VERSION_NUMBER < 0x10100000L
#ifdef SSL3_FLAGS_NO_RENEGOTIATE_CIPHERS
        if (conn->ssl->s3)
        {
            conn->ssl->s3->flags |= SSL3_FLAGS_NO_RENEGOTIATE_CIPHERS;
        }
#endif
#endif
        return SW_READY;
    }
    /**
     * The TLS/SSL handshake was not successful but was shutdown.
     */
    else if (n == 0)
    {
        return SW_ERROR;
    }

    long err = SSL_get_error(conn->ssl, n);
    if (err == SSL_ERROR_WANT_READ)
    {
        return SW_WAIT;
    }
    else if (err == SSL_ERROR_WANT_WRITE)
    {
        return SW_WAIT;
    }
    else if (err == SSL_ERROR_SSL)
    {
        swWarn("bad SSL client[%s:%d].", swConnection_get_ip(conn), swConnection_get_port(conn));
        return SW_ERROR;
    }
    //EOF was observed
    else if (err == SSL_ERROR_SYSCALL && n == 0)
    {
        return SW_ERROR;
    }
    swWarn("SSL_do_handshake() failed. Error: %s[%ld|%d].", strerror(errno), err, errno);
    return SW_ERROR;
}
  • 握手成功以後,若是設置了雙向加密,還要調用 swSSL_get_client_certificate 函數獲取客戶端的證書文件,而後將證書文件發送給 worker 進程。
  • swSSL_get_client_certificate 函數中首先利用 SSL_get_peer_certificate 來獲取客戶端的證書,而後利用 PEM_write_bio_X509 將證書與 BIO 對象綁定,最後利用 BIO_read 函數將證書寫到內存中。
int swSSL_get_client_certificate(SSL *ssl, char *buffer, size_t length)
{
    long len;
    BIO *bio;
    X509 *cert;

    cert = SSL_get_peer_certificate(ssl);
    if (cert == NULL)
    {
        return SW_ERR;
    }

    bio = BIO_new(BIO_s_mem());
    if (bio == NULL)
    {
        swWarn("BIO_new() failed.");
        X509_free(cert);
        return SW_ERR;
    }

    if (PEM_write_bio_X509(bio, cert) == 0)
    {
        swWarn("PEM_write_bio_X509() failed.");
        goto failed;
    }

    len = BIO_pending(bio);
    if (len < 0 && len > length)
    {
        swWarn("certificate length[%ld] is too big.", len);
        goto failed;
    }

    int n = BIO_read(bio, buffer, len);

    BIO_free(bio);
    X509_free(cert);

    return n;

    failed:

    BIO_free(bio);
    X509_free(cert);

    return SW_ERR;
}

worker 進程,接到了 SW_EVENT_CONNECT 事件以後,會把證書文件存儲在 ssl_client_cert.str 中。當鏈接關閉時,會釋放 ssl_client_cert.str 內存。值得注意的是,此時驗證鏈接有效的函數是 swServer_connection_verify_no_ssl。此函數不會驗證 SSL 此時的狀態,只會驗證鏈接與 session 的有效性。

int swWorker_onTask(swFactory *factory, swEventData *task)
{
    ...
    
    switch (task->info.type)
    {
        ...
        
        case SW_EVENT_CLOSE:
 #ifdef SW_USE_OPENSSL
        conn = swServer_connection_verify_no_ssl(serv, task->info.fd);
        if (conn && conn->ssl_client_cert.length > 0)
        {
            sw_free(conn->ssl_client_cert.str);
            bzero(&conn->ssl_client_cert, sizeof(conn->ssl_client_cert.str));
        }
#endif
        factory->end(factory, task->info.fd);
        break;

    case SW_EVENT_CONNECT:
 #ifdef SW_USE_OPENSSL
        //SSL client certificate
        if (task->info.len > 0)
        {
            conn = swServer_connection_verify_no_ssl(serv, task->info.fd);
            conn->ssl_client_cert.str = sw_strndup(task->data, task->info.len);
            conn->ssl_client_cert.size = conn->ssl_client_cert.length = task->info.len;
        }
#endif
        if (serv->onConnect)
        {
            serv->onConnect(serv, &task->info);
        }
        break;
        
        ...
    }
}

static sw_inline swConnection *swServer_connection_verify_no_ssl(swServer *serv, uint32_t session_id)
{
    swSession *session = swServer_get_session(serv, session_id);
    int fd = session->fd;
    swConnection *conn = swServer_connection_get(serv, fd);
    if (!conn || conn->active == 0)
    {
        return NULL;
    }
    if (session->id != session_id || conn->session_id != session_id)
    {
        return NULL;
    }
    return conn;
}
  • 當鏈接創建以後,就要經過 SSL 加密隧道讀取數據,最基礎簡單的接受函數是 swPort_onRead_raw 函數,該函數會最終調用 swSSL_recv 函數,與 SSL_write 相似,SSL_read 會自動從 ssl 中讀取加密數據,並將解密後的數據存儲起來,等待發送給 worker 進程,進行具體的邏輯。
static int swPort_onRead_raw(swReactor *reactor, swListenPort *port, swEvent *event)
{
    n = swConnection_recv(conn, task.data.data, SW_BUFFER_SIZE, 0);
}

static sw_inline ssize_t swConnection_recv(swConnection *conn, void *__buf, size_t __n, int __flags)
{
    _recv:
    if (conn->ssl)
    {
        ssize_t ret = 0;
        size_t n_received = 0;

        while (n_received < __n)
        {
            ret = swSSL_recv(conn, ((char*)__buf) + n_received, __n - n_received);
            if (__flags & MSG_WAITALL)
            {
                if (ret <= 0)
                {
                    retval = ret;
                    goto _return;
                }
                else
                {
                    n_received += ret;
                }
            }
            else
            {
                retval = ret;
                goto _return;
            }
        }

        retval = n_received;
    }

    if (retval < 0 && errno == EINTR)
    {
        goto _recv;
    }
    else
    {
        goto _return;
    }
    
    _return:
    
    return retval;
}

ssize_t swSSL_recv(swConnection *conn, void *__buf, size_t __n)
{
    int n = SSL_read(conn->ssl, __buf, __n);
    if (n < 0)
    {
        int _errno = SSL_get_error(conn->ssl, n);
        switch (_errno)
        {
        case SSL_ERROR_WANT_READ:
            conn->ssl_want_read = 1;
            errno = EAGAIN;
            return SW_ERR;

        case SSL_ERROR_WANT_WRITE:
            conn->ssl_want_write = 1;
            errno = EAGAIN;
            return SW_ERR;

        case SSL_ERROR_SYSCALL:
            return SW_ERR;

        case SSL_ERROR_SSL:
            swSSL_connection_error(conn);
            errno = SW_ERROR_SSL_BAD_CLIENT;
            return SW_ERR;

        default:
            break;
        }
    }
    return n;
}

相應的,worker 進程在接受到數據以後,要經過 swServer_connection_verify 函數驗證 SSL 鏈接的狀態,若是發送數據的鏈接狀態並非 SW_SSL_STATE_READY,就會拋棄數據。

int swWorker_onTask(swFactory *factory, swEventData *task)
{
    ...
    
    switch (task->info.type)
    {
        case SW_EVENT_TCP:
    //ringbuffer shm package
    case SW_EVENT_PACKAGE:
        //discard data
        if (swWorker_discard_data(serv, task) == SW_TRUE)
        {
            break;
        }
        
        ...

    //chunk package
    case SW_EVENT_PACKAGE_START:
    case SW_EVENT_PACKAGE_END:
        //discard data
        if (swWorker_discard_data(serv, task) == SW_TRUE)
        {
            break;
        }
        package = swWorker_get_buffer(serv, task->info.from_id);
        if (task->info.len > 0)
        {
            //merge data to package buffer
            swString_append_ptr(package, task->data, task->info.len);
        }
        //package end
        if (task->info.type == SW_EVENT_PACKAGE_END)
        {
            goto do_task;
        }
        break;
        
        ...
    }
}

static sw_inline int swWorker_discard_data(swServer *serv, swEventData *task)
{
    swConnection *conn = swServer_connection_verify(serv, session_id);
    
    ...

}

static sw_inline swConnection *swServer_connection_verify(swServer *serv, int session_id)
{
    swConnection *conn = swServer_connection_verify_no_ssl(serv, session_id);
#ifdef SW_USE_OPENSSL
    if (!conn)
    {
        return NULL;
    }
    if (conn->ssl && conn->ssl_state != SW_SSL_STATE_READY)
    {
        swoole_error_log(SW_LOG_NOTICE, SW_ERROR_SSL_NOT_READY, "SSL not ready");
        return NULL;
    }
#endif
    return conn;

}
相關文章
相關標籤/搜索