Swoole 原始碼分析——Server 模組之 OpenSSL (下)

leoyang發表於2018-09-22

前言

上一篇文章我們講了 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;

}
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章