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
(1)
session id
會話複用。對於已經建立的
SSL
會話,使用session id
為key
(session id
來自第一次請求的server hello
中的session id
欄位),主金鑰為value
組成一對鍵值,儲存在本地,伺服器和客戶端都儲存一份。當第二次握手時,客戶端若想使用會話複用,則發起的
client hello
中session id
會置上對應的值,伺服器收到這個client hello
,解析session id
,查詢本地是否有該session id
,如果有,判斷當前的加密套件和上個會話的加密套件是否一致,一致則允許使用會話複用,於是自己的server hello
中session id
也置上和client hello
中一樣的值。然後計算對稱祕鑰,解析後續的操作。如果伺服器未查到客戶端的
session id
指定的會話(可能是會話已經老化),則會重新握手,session id
要麼重新計算(和client hello
中session 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,回去進行解密、解碼能相關操作,來恢復會話資訊。如果能夠恢復會話資訊,那麼久提取會話資訊的主金鑰進行後續的操作。
-
stapling
與stapling_verify
:OCSP
(Online Certificate Status Protocol
,線上證書狀態協議)是用來檢驗證書合法性的線上查詢服務,一般由證書所屬CA
提供。假如服務端的私鑰被洩漏,對應的證書就會被加入黑名單,為了驗證服務端的證書是否在黑名單中,某些客戶端會在
TLS
握手階段進一步協商時,實時查詢OCSP
介面,並在獲得結果前阻塞後續流程。OCSP
查詢本質是一次完整的HTTP
請求 - 響應,這中間DNS
查詢、建立TCP
、服務端處理等環節都可能耗費很長時間,導致最終建立TLS
連線時間變得更長。而
OCSP Stapling
(OCSP
封套),是指服務端主動獲取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,其中很多配置對於最新版本來說,沒有任何影響,僅僅作為相容舊版本而保留。
-
SSL
的KEY
檔案一般都是由對稱加密演算法所加密,這時候就需要呼叫SSL_CTX_set_default_passwd_cb
與SSL_CTX_set_default_passwd_cb_userdata
,否則在啟動swoole
的時候,就需要手動在命令列中輸入該密碼。 - 接著就需要將私鑰檔案和證書檔案的路徑傳入
SSL
,相應的函式是SSL_CTX_use_certificate_file
、SSL_CTX_use_certificate_chain_file
與SSL_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_METHOD
。swoole
預設使用 SW_SSLv23_METHOD
,該方法支援 SSLv2
與 SSLv3
:
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_file
、SSL_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
函式:
-
NPN
與ALPN
都是為了支援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
繫結 SSL
,SSL_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_send
、SSL_write
。SSL_write
發生錯誤之後,函式會返回SSL_ERROR_WANT_READ
、SSL_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;
}