摘要:對于服務端來說,緩存默認是不能使用的,可以通過調用函數來進行設置生效。在回調函數中,首先申請一個大數數據結構,然后將其設定為,該值表示公鑰指數,然后利用函數生成秘鑰。此時需要調用函數將新的連接與綁定。
前言
上一篇文章我們講了 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; } #endifsession 會話重用
所有的 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; }
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/31994.html
摘要:當此時的套接字不可寫的時候,會自動放入緩沖區中。當大于高水線時,會自動調用回調函數。寫就緒狀態當監控到套接字進入了寫就緒狀態時,就會調用函數。如果為,說明此時異步客戶端雖然建立了連接,但是還沒有調用回調函數,因此這時要調用函數。 前言 上一章我們說了客戶端的連接 connect,對于同步客戶端來說,連接已經建立成功;但是對于異步客戶端來說,此時可能還在進行 DNS 的解析,on...
摘要:兩個函數是可選回調函數。附帶了一組可信任證書。應該注意的是,驗證失敗并不意味著連接不能使用。在對證書進行驗證時,有一些安全性檢查并沒有執行,包括證書的失效檢查和對證書中通用名的有效性驗證。 前言 swoole_client 提供了 tcp/udp socket 的客戶端的封裝代碼,使用時僅需 new swoole_client 即可。 swoole 的 socket client 對比...
摘要:另一方比如小明得到公鑰之后,雙方就可以通信。然而,中間人還是可能截獲公鑰,然后自己弄一對秘鑰,然后告訴小明說是小紅的公鑰。這樣,小亮在簽署小紅的身份證的時候,可以在小紅身份證后面附上自己的身份證。一般來說,自簽名的根身份證用于公司內部使用。 前言 自從 Lets Encrypt 上線之后,HTTPS 網站數量占比越來越高,相信不久的未來就可以實現全網 HTTPS,大部分主流瀏覽器也對 ...
摘要:判斷客戶端是否配置了檢測或者長度檢測,如果配置了就調用接受完整的數據包,這兩天會調用,進而調用函數。異步客戶端接受數據異步的客戶端接受數據調用的和同步的客戶端相同,都是調用函數。 recv 接受數據 客戶端接受數據需要指定緩存區最大長度,就是下面的 buf_len,flags 用于指定是否設置 waitall 標志,如果設定了 waitall 就必須設定準確的 size,否則會一直等...
摘要:如果在調用之前我們設置了,但是不在第二個進程啟動前這個套接字,那么第二個進程仍然會在調用函數的時候出錯。 前言 本節主要介紹 server 模塊進行初始化的代碼,關于初始化過程中,各個屬性的意義,可以參考官方文檔: SERVER 配置選項 關于初始化過程中,用于監聽的 socket 綁定問題,可以參考: UNP 學習筆記——基本 TCP 套接字編程 UNP 學習筆記——套接字選項 構造...
閱讀 1916·2021-11-23 09:51
閱讀 1245·2019-08-30 15:55
閱讀 1612·2019-08-30 15:44
閱讀 758·2019-08-30 14:11
閱讀 1145·2019-08-30 14:10
閱讀 914·2019-08-30 13:52
閱讀 2629·2019-08-30 12:50
閱讀 614·2019-08-29 15:04