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

前言

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

OpenSSL 初始化

swoole 中,如果想要進行 ssl 加密,只需要如下設置即可:

$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 起了什麼作用:

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 的參數。

  • 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,這兩個方式都會告訴客戶端這次會話不進行會話複用。

    (2) session ticket 會話複用

    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;

}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章