前言
上一篇文章我們講了 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 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;
}
本作品採用《CC 協議》,轉載必須註明作者和本文連結