TLS握手的OpenSSL實現(深度1)
我們跳過握手的總狀態機和讀寫狀態機,因為我認為那是OpenSSL架構方案的一個敗筆,邏輯非常的不清晰,是程序員思維,而不是正常的邏輯思維。與握手邏輯比較相關的在statem_clnt.c和statem_srv.c中。分別是客戶端的握手邏輯和服務端的握手邏輯。
我們以服務端為重點來分析。

一個簡單的函數列表就能看出來其中的大體邏輯。對於服務器來說,是被動的處理消息,響應客戶端請求的一方。所以所有的發送都是有接收來觸發,接收對應的函數典型的就是tls_process_開頭的函數,而發送所對應的函數就是tls_construct對應的函數。
我們知道TLS 1.2的握手流程,這個流程終將要體現在代碼之中。並且會同時體現在客戶端的代碼和服務端的代碼兩部分。兩部分的代碼雖然都位於OpenSSL中,但是實際在運行的時候,OpenSSL要么作為客戶端要么作為服務端來運行。
服務端在監聽用戶請求的時候,第一個消息肯定是接收客戶端發送來的Client Hello消息,並且對應的處理和返回Server Hello消息。如果服務端發現了會話復用的可能,在這一步會做出決策,並且直接影響了后續服務端要發送的消息類型。如果決定復用會話,服務端就不會發送Server Key Exchange消息。整個消息處理引擎的入口是ossl_statem_server_process_message函數,該函數如下:
MSG_PROCESS_RETURN ossl_statem_server_process_message(SSL *s, PACKET *pkt){
OSSL_STATEM *st = &s->statem;
switch (st->hand_state) {
case TLS_ST_SR_CLNT_HELLO:
return tls_process_client_hello(s, pkt);
case TLS_ST_SR_CERT:
return tls_process_client_certificate(s, pkt);
case TLS_ST_SR_KEY_EXCH:
return tls_process_client_key_exchange(s, pkt);
case TLS_ST_SR_CERT_VRFY:
return tls_process_cert_verify(s, pkt);
#ifndef OPENSSL_NO_NEXTPROTONEG
case TLS_ST_SR_NEXT_PROTO:
return tls_process_next_proto(s, pkt);
#endif
case TLS_ST_SR_CHANGE:
return tls_process_change_cipher_spec(s, pkt);
case TLS_ST_SR_FINISHED:
return tls_process_finished(s, pkt);
default:
break;
}
return MSG_PROCESS_ERROR;
}
可以看到SSL結構體的statem域在這個時候會被取出來使用,這個域對應的結構體是OSSL_STATEM。st->hand_state里面就是當前服務器處理的客戶端的握手狀態。收到第一個Client Hello包的時候,狀態是TLS_ST_SR_CLNT_HELLO,所以會首先進入 tls_process_client_hello(s, pkt);函數。所有的消息處理函數的輸入參數都是SSL結構體和當前接收到的數據包的內容。
Client Helllo的處理是一個很復雜的過程,因為在TLS協議的發展過程中,誕生了很多的各種各樣的擴展,這些擴展大都會體現在Hello消息的擴展頭部中。而且還需要兼容很老的握手方式,所以顯得比較臃腫。
MSG_PROCESS_RETURN tls_process_client_hello(SSL *s, PACKET *pkt)
{
int i, al = SSL_AD_INTERNAL_ERROR;
unsigned int j, complen = 0;
unsigned long id;
const SSL_CIPHER *c;
#ifndef OPENSSL_NO_COMP
SSL_COMP *comp = NULL;
#endif
STACK_OF(SSL_CIPHER) *ciphers = NULL; //STACK_OF相當於一個數組
int protverr;
/* |cookie|只在 DTLS有用 */
PACKET session_id, cipher_suites, compression, extensions, cookie;
int is_v2_record;
static const unsigned char null_compression = 0;
is_v2_record = RECORD_LAYER_is_sslv2_record(&s->rlayer); //是否是SSLv2的標志
PACKET_null_init(&cookie);
if (is_v2_record) {
//SSLv2的邏輯不參與分析
} else {
/*這里獲得客戶端使用的TLS版本號,使用Client Hello消息內部定義的版本號,而不是使用Record層頭部的版本號,根據RFC 2246,這兩個版本號在TLS 1.0中是可以不同的。PACKET_開頭的函數都是對數據包的處理結構體對應的封裝,像內核的sk_buf,可以用來一步一步的向前處理數據包,或者是對數據包做其他的操作。SSLerr是OpenSSL的錯誤報告系統,每一個錯誤都實際的對應一個錯誤描述的字符串。比如Nginx在使用OpenSSL的時候也可以通過API來獲得OpenSSL所報告錯誤的返回字符串。這個錯誤子系統對於OpenSSL來說是可以穿過OpenSSL的庫本身在外部被得到的。*/
if (!PACKET_get_net_2(pkt, (unsigned int *)&s->client_version)) {
al = SSL_AD_DECODE_ERROR;
SSLerr(SSL_F_TLS_PROCESS_CLIENT_HELLO, SSL_R_LENGTH_TOO_SHORT);
goto f_err;
}
}
/*根據得到的TLS版本號改變SSL結構體上下文的版本號,如果發現版本號太低,就拒絕提供服務了*/
if (!SSL_IS_DTLS(s)) {
protverr = ssl_choose_server_version(s);
} else if (s->method->version != DTLS_ANY_VERSION &&
DTLS_VERSION_LT(s->client_version, s->version)) {
protverr = SSL_R_VERSION_TOO_LOW;
} else {
protverr = 0;
}
if (protverr) {
SSLerr(SSL_F_TLS_PROCESS_CLIENT_HELLO, protverr);
if ((!s->enc_write_ctx && !s->write_hash)) {
s->version = s->client_version;
}
al = SSL_AD_PROTOCOL_VERSION;
goto f_err;
}
if (is_v2_record) {
/*SSLv2的邏輯不納入分析*/
} else {
/* 開始處理消息的內容。第一步是處理客戶端的隨機數*/
if (!PACKET_copy_bytes(pkt, s->s3->client_random, SSL3_RANDOM_SIZE)
|| !PACKET_get_length_prefixed_1(pkt, &session_id)) {
al = SSL_AD_DECODE_ERROR;
SSLerr(SSL_F_TLS_PROCESS_CLIENT_HELLO, SSL_R_LENGTH_MISMATCH);
goto f_err;
}
//然后是獲得Session ID
if (PACKET_remaining(&session_id) > SSL_MAX_SSL_SESSION_ID_LENGTH) {
al = SSL_AD_DECODE_ERROR;
SSLerr(SSL_F_TLS_PROCESS_CLIENT_HELLO, SSL_R_LENGTH_MISMATCH);
goto f_err;
}
if (SSL_IS_DTLS(s)) {
//DTLS不納入分析
}
//接下來是客戶端所支持的加密套件的列表的解析
if (!PACKET_get_length_prefixed_2(pkt, &cipher_suites)
|| !PACKET_get_length_prefixed_1(pkt, &compression)) {
al = SSL_AD_DECODE_ERROR;
SSLerr(SSL_F_TLS_PROCESS_CLIENT_HELLO, SSL_R_LENGTH_MISMATCH);
goto f_err;
}
/*最后獲得擴展列表,啟動擴展列表的分析*/
extensions = *pkt;
}
if (SSL_IS_DTLS(s)) {
//DTLS不納入分析
}
s->hit = 0; //hit域是SSL結構體里用來表達是否命中Session Cache的域,在查找之前,先初始化為0
if (is_v2_record ||
(s->new_session &&
(s->options & SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION))) {
if (!ssl_get_new_session(s, 1))
goto err;
} else {
//針對客戶端發來的Session ID啟動Seccion Cache的查找,這里的查找同時傳入了擴展頭部,這個目的是為了獲得Session Ticket。也就是說,Session Cache和Session Ticket被當做了Cache內部的事情,對握手過程暴露的是同一個接口。這是因為Session Ticket功能也是后來開發出來的新事物,這也是在原有的代碼基礎上修改的結果。如果沒有找到對應的Session,就創建一個Session。每一個沒有找到Session緩存的連接都會創建Session,但是並不是每一個Session都會進入Session Cache以備后續的查找。用戶可以使用API人為的關閉OpenSSL的Session Cache功能。如果在Session Cache中找到了緩存的Session,s->hit就會被置為1,s->session會被設置為查找到的SSL_SESSION結構體。如果沒有找到,hit雖然不會設置為1,但是s->session仍然指向新創建的SSL_SESSION結構體
i = ssl_get_prev_session(s, &extensions, &session_id);
if (i == 1 && s->version == s->session->ssl_version) {
s->hit = 1;
} else if (i == -1) {
goto err;
} else {
if (!ssl_get_new_session(s, 1))
goto err;
}
}
//接下來是實際的開始解析加密套件的列表。這個解析得到的結果是放到s->s3->tmp中的。這個SSL的s3結構體會經常遇到,握手的過程中會比較頻繁的修改這個域,這個域的名字顯然是一個歷史遺留問題,不必拘泥。tmp的名字也能看出來它的意義,它用於存儲握手過程中臨時用的數據,比如用來存儲加密套件列表的原始數據的長度等信息。這一步解析的結果不是直接存儲進SSL結構體的,而是存儲在傳入進解析函數的ciphers數組指針中。
if (ssl_bytes_to_cipher_list(s, &cipher_suites, &(ciphers),
is_v2_record, &al) == NULL) {
goto f_err;
}
/*如果從Session Cache中找到了對應的Session,這個Session中描述的對稱加密的方法就必須要能夠匹配客戶端發來的支持的對稱加密套件,在這里緊接着進行檢查*/
if (s->hit) {
j = 0;
//s->session->cipher->id里面存放的就是當前的Session所對應的密碼學套件的ID。通過這個值與客戶端發來的密碼學套件列表的循環對比,就能檢查得到是否匹配的結論。
id = s->session->cipher->id;
for (i = 0; i < sk_SSL_CIPHER_num(ciphers); i++) {
c = sk_SSL_CIPHER_value(ciphers, i);
if (c->id == id) {
j = 1;
break;
}
}
//j==0就代表了沒有找到,這個時候握手就錯誤了
if (j == 0) {
al = SSL_AD_ILLEGAL_PARAMETER;
SSLerr(SSL_F_TLS_PROCESS_CLIENT_HELLO,
SSL_R_REQUIRED_CIPHER_MISSING);
goto f_err;
}
}
//TLS握手的頭部是允許壓縮的,但是實際中各個客戶端都禁止掉了壓縮,所以這里得到的complen的值一般是0,緊接着的循環也就不會發生
complen = PACKET_remaining(&compression);
for (j = 0; j < complen; j++) {
if (PACKET_data(&compression)[j] == 0)
break;
}
if (j >= complen) {
al = SSL_AD_DECODE_ERROR;
SSLerr(SSL_F_TLS_PROCESS_CLIENT_HELLO, SSL_R_NO_COMPRESSION_SPECIFIED);
goto f_err;
}
/* TLS擴展是在SSL3之后才有的,所以這里對擴展頭部的解析要判斷版本。解析的結果存放到extensions里面,這個函數只是單純的解析,並沒有進行執行*/
if (s->version >= SSL3_VERSION) {
if (!ssl_parse_clienthello_tlsext(s, &extensions)) {
SSLerr(SSL_F_TLS_PROCESS_CLIENT_HELLO, SSL_R_PARSE_TLSEXT);
goto err;
}
}
/*生成服務器端的隨機數,這個隨機數會在Server Hello中發送返還給Client
*/
{
unsigned char *pos;
pos = s->s3->server_random;
if (ssl_fill_hello_random(s, 1, pos, SSL3_RANDOM_SIZE) <= 0) {
goto f_err;
}
}
//處理用戶注冊的Session回調函數。用戶端可以實現自己的Session Cache,就像Nginx那樣。實現的方法就是向OpenSSL注冊自己的回調函數,這個回調函數就會在這里被調用。所以如果關閉了OpenSSL內部的Session Cache,並且注冊了自己的回調函數,就可以實現OpenSSL意外的Session Cache功能。
if (!s->hit && s->version >= TLS1_VERSION && s->tls_session_secret_cb) {
const SSL_CIPHER *pref_cipher = NULL;
s->session->master_key_length = sizeof(s->session->master_key);
if (s->tls_session_secret_cb(s, s->session->master_key,
&s->session->master_key_length, ciphers,
&pref_cipher,
s->tls_session_secret_cb_arg)) {
s->hit = 1;
s->session->ciphers = ciphers;
s->session->verify_result = X509_V_OK;
ciphers = NULL;
pref_cipher =
pref_cipher ? pref_cipher : ssl3_choose_cipher(s,
s->
session->ciphers,
SSL_get_ciphers
(s));
if (pref_cipher == NULL) {
al = SSL_AD_HANDSHAKE_FAILURE;
SSLerr(SSL_F_TLS_PROCESS_CLIENT_HELLO, SSL_R_NO_SHARED_CIPHER);
goto f_err;
}
s->session->cipher = pref_cipher;
sk_SSL_CIPHER_free(s->cipher_list);
s->cipher_list = sk_SSL_CIPHER_dup(s->session->ciphers);
sk_SSL_CIPHER_free(s->cipher_list_by_id);
s->cipher_list_by_id = sk_SSL_CIPHER_dup(s->session->ciphers);
}
}
s->s3->tmp.new_compression = NULL;
#ifndef OPENSSL_NO_COMP
/* This only happens if we have a cache hit */
if (s->session->compress_meth != 0) {
//TLS壓縮不納入分析
} else if (s->hit)
comp = NULL;
else if (ssl_allow_compression(s) && s->ctx->comp_methods) {
//TLS壓縮不納入分析
}
#else
if (s->session->compress_meth != 0) {
SSLerr(SSL_F_TLS_PROCESS_CLIENT_HELLO, SSL_R_INCONSISTENT_COMPRESSION);
goto f_err;
}
#endif
//設置cipher list到SSL的結構體,注意這一步並沒有進行密碼套件的選擇
if (!s->hit) {
#ifdef OPENSSL_NO_COMP
s->session->compress_meth = 0;
#else
s->session->compress_meth = (comp == NULL) ? 0 : comp->id;
#endif
sk_SSL_CIPHER_free(s->session->ciphers);
s->session->ciphers = ciphers;
if (ciphers == NULL) {
al = SSL_AD_INTERNAL_ERROR;
SSLerr(SSL_F_TLS_PROCESS_CLIENT_HELLO, ERR_R_INTERNAL_ERROR);
goto f_err;
}
ciphers = NULL;
if (!tls1_set_server_sigalgs(s)) {
SSLerr(SSL_F_TLS_PROCESS_CLIENT_HELLO, SSL_R_CLIENTHELLO_TLSEXT);
goto err;
}
}
sk_SSL_CIPHER_free(ciphers);
return MSG_PROCESS_CONTINUE_PROCESSING;
f_err:
ssl3_send_alert(s, SSL3_AL_FATAL, al);
err:
ossl_statem_set_error(s);
sk_SSL_CIPHER_free(ciphers);
return MSG_PROCESS_ERROR;
}
但是這還不是服務器處理Client Hello的全部邏輯。因為這個TLS握手狀態機的上層還有一個狀態機,那個讀取狀態機定義了兩個函數執行,一個是處理消息的函數,一個是處理消息之后要執行的函數,分別是ossl_statem_server_process_message和ossl_statem_server_post_process_message,剛看到的是第一個處理函數對應的處理分支,處理完之后會調用下一個處理函數。
WORK_STATE ossl_statem_server_post_process_message(SSL *s, WORK_STATE wst)
{
OSSL_STATEM *st = &s->statem;
switch (st->hand_state) {
case TLS_ST_SR_CLNT_HELLO:
return tls_post_process_client_hello(s, wst);
case TLS_ST_SR_KEY_EXCH:
return tls_post_process_client_key_exchange(s, wst);
default:
break;
}
return WORK_ERROR;
}
可以看到這個處理函數只會在兩種情況下有效,其中一種就是收到Client Hello的時候。
WORK_STATE tls_post_process_client_hello(SSL *s, WORK_STATE wst)
{
int al = SSL_AD_HANDSHAKE_FAILURE;
const SSL_CIPHER *cipher;
if (wst == WORK_MORE_A) {
if (!s->hit) {
/* 對證書的處理也是允許用戶注冊自己的處理函數,這里會進行調用*/
if (s->cert->cert_cb) {
int rv = s->cert->cert_cb(s, s->cert->cert_cb_arg);
if (rv == 0) {
al = SSL_AD_INTERNAL_ERROR;
SSLerr(SSL_F_TLS_POST_PROCESS_CLIENT_HELLO,
SSL_R_CERT_CB_ERROR);
goto f_err;
}
if (rv < 0) {
s->rwstate = SSL_X509_LOOKUP;
return WORK_MORE_A;
}
s->rwstate = SSL_NOTHING;
}
//之前在解析的時候並沒有實際的選擇使用哪一個密碼學套件,在這里進行最終的選擇
cipher =
ssl3_choose_cipher(s, s->session->ciphers, SSL_get_ciphers(s));
if (cipher == NULL) {
SSLerr(SSL_F_TLS_POST_PROCESS_CLIENT_HELLO,
SSL_R_NO_SHARED_CIPHER);
goto f_err;
}
//最后選擇的結果仍然是放在了s->s3->tmp.new_cipher 中,作為臨時數據存在。
s->s3->tmp.new_cipher = cipher;
/* OpenSSL允許客戶端注冊是否允許產生Session Ticket,也就是用戶可以控制什么時候產生Session Ticket,什么時候不產生,因為每次產生Session Ticket對於服務器來說也是一種資源消耗的行為,如果遇到惡意攻擊,用戶是在一些特定的情況下不要生成Session Ticket的 */
if (s->not_resumable_session_cb != NULL)
s->session->not_resumable = s->not_resumable_session_cb(s,
((cipher->algorithm_mkey & (SSL_kDHE | SSL_kECDHE)) != 0));
if (s->session->not_resumable)
/* do not send a session ticket */
s->tlsext_ticket_expected = 0;
} else {
/* Session-id reuse */
s->s3->tmp.new_cipher = s->session->cipher;
}
if (!(s->verify_mode & SSL_VERIFY_PEER)) {
if (!ssl3_digest_cached_records(s, 0)) {
al = SSL_AD_INTERNAL_ERROR;
goto f_err;
}
}
/*-
* we now have the following setup.
* client_random
* cipher_list - our preferred list of ciphers
* ciphers - the clients preferred list of ciphers
* compression - basically ignored right now
* ssl version is set - sslv3
* s->session - The ssl session has been setup.
* s->hit - session reuse flag
* s->s3->tmp.new_cipher- the new cipher to use.
*/
/*這個時候,我們已經解析得到了很多的信息,接下來要處理的就是其他的各種各樣的擴展頭部了。這一步的檢查就是檢查當前是否要進行OCSP的檢查。這個是否要進行是繼承自SSL_CTX的,這個配置是生成上下文的時候由用戶生成的。在Nginx的情況就是Nginx的OCSP的相關配置決定的*/
if (s->version >= SSL3_VERSION) {
if (!ssl_check_clienthello_tlsext_late(s, &al)) {
SSLerr(SSL_F_TLS_POST_PROCESS_CLIENT_HELLO,
SSL_R_CLIENTHELLO_TLSEXT);
goto f_err;
}
}
wst = WORK_MORE_B;
}
s->renegotiate = 2;
return WORK_FINISHED_STOP;
f_err:
ssl3_send_alert(s, SSL3_AL_FATAL, al);
ossl_statem_set_error(s);
return WORK_ERROR;
}
至此一個Client Hello已經處理完了。看到這里肯定會疑惑,為什么要分成兩個部分來處理。軟件就是這樣,很多決策是沒有什么太顯然的理由的。分成了兩個就分成了兩個,要合並成一個也並沒有什么所謂。可以理解成是一個是處理普通頭的,一個是處理擴展的。不過這樣理解也會顯得牽強。
處理完Client Hello之后,肯定就是要構造Server Hello消息了。
int tls_construct_server_hello(SSL *s)
{
unsigned char *buf;
unsigned char *p, *d;
int i, sl;
int al = 0;
unsigned long l;
/*我們知道數據包包含了頭部和數據兩個部分,在構造的時候是分別構造的,一般的頭部部分構造比較復雜,數據部分通常就是一個拷貝操作。這里的 ssl_handshake_start就是一個區分頭部和數據部分的指針。# define ssl_handshake_start(s) (((unsigned char *)s->init_buf->data) + s->method->ssl3_enc->hhlen)*/
buf = (unsigned char *)s->init_buf->data;
d = p = ssl_handshake_start(s);
//拿到了頭部的指針之后,就可以開始往里順序的填充頭部了。首先填充的是版本號
*(p++) = s->version >> 8;
*(p++) = s->version & 0xff;
/*
* 填充服務端產生的隨機數
*/
memcpy(p, s->s3->server_random, SSL3_RANDOM_SIZE);
p += SSL3_RANDOM_SIZE;
/*-
接下來是處理Session ID。用戶來了Session ID的請求,有兩種方式,一種是Session Cache,一種是Session Ticket,如果Session Ticket出現,就會跳過Session Cache而優先采用Session Ticket。處理用戶發來的Session ID的請求也有好幾種情況,一種是找到了用戶Session ID對應的Session,這時就會回復這個Session ID。還有一種是沒有找到,這時就會創建一個新的Session ID,發送回去的是新的Session ID。由於Session Ticket是比Session ID有更高的優先級。如果是服務器決定使用Session Ticket,就會回復生成的Session Ticket。如果想要一個Session ID不能被復用,就會回復一個0長度的Session ID
*/
if (s->session->not_resumable ||
(!(s->ctx->session_cache_mode & SSL_SESS_CACHE_SERVER)
&& !s->hit))
s->session->session_id_length = 0;
sl = s->session->session_id_length;
if (sl > (int)sizeof(s->session->session_id)) {
SSLerr(SSL_F_TLS_CONSTRUCT_SERVER_HELLO, ERR_R_INTERNAL_ERROR);
ossl_statem_set_error(s);
return 0;
}
*(p++) = sl;
memcpy(p, s->session->session_id, sl);
p += sl;
//向Server Hello消息中寫入服務器選擇的加密套件
/* put the cipher */
i = ssl3_put_cipher_by_char(s->s3->tmp.new_cipher, p);
p += i;
/* put the compression method */
#ifdef OPENSSL_NO_COMP
*(p++) = 0;
#else
if (s->s3->tmp.new_compression == NULL)
*(p++) = 0;
else
*(p++) = s->s3->tmp.new_compression->id;
#endif
//寫入服務器支持的Server Hello的擴展頭部
if (ssl_prepare_serverhello_tlsext(s) <= 0) {
SSLerr(SSL_F_TLS_CONSTRUCT_SERVER_HELLO, SSL_R_SERVERHELLO_TLSEXT);
ossl_statem_set_error(s);
return 0;
}
if ((p =
ssl_add_serverhello_tlsext(s, p, buf + SSL3_RT_MAX_PLAIN_LENGTH,
&al)) == NULL) {
ssl3_send_alert(s, SSL3_AL_FATAL, al);
SSLerr(SSL_F_TLS_CONSTRUCT_SERVER_HELLO, ERR_R_INTERNAL_ERROR);
ossl_statem_set_error(s);
return 0;
}
/* 填充完數據之后就是設置頭部的收尾工作了,比如長度信息*/
l = (p - d);
if (!ssl_set_handshake_header(s, SSL3_MT_SERVER_HELLO, l)) {
SSLerr(SSL_F_TLS_CONSTRUCT_SERVER_HELLO, ERR_R_INTERNAL_ERROR);
ossl_statem_set_error(s);
return 0;
}
return 1;
}
Server Hello就沒有后續的步驟了。一個函數就可以完成填充,填充完成之后就可以發出去了。發出去的過程也會改變整個狀態機的狀態。使得SSL進入接收狀態。
在TLS 1.2中,Server在發送了Server Hello之后是不會等待客戶端的回復的。而是選擇是否繼續發送其他的消息,例如上面看到過是否會發送一個New Ticket消息的判斷。這個判斷是建立在對Session的一整套邏輯的判斷的基礎上的。
如果決定了復用,接下來Server就不會發送Server Key Exchange,否則Server就該發送該消息溝通密碼學上下文。
//這里省略了PSK相關的邏輯。PSK是Pre Shared Key,就是雙方互相提前知道了約定密碼的溝通方法,一般不使用。SRP也不討論。
int tls_construct_server_key_exchange(SSL *s)
{
#ifndef OPENSSL_NO_DH
EVP_PKEY *pkdh = NULL;
int j;
#endif
#ifndef OPENSSL_NO_EC
unsigned char *encodedPoint = NULL;
int encodedlen = 0;
int curve_id = 0;
#endif
EVP_PKEY *pkey;
const EVP_MD *md = NULL;
unsigned char *p, *d;
int al, i;
unsigned long type;
int n;
const BIGNUM *r[4];
int nr[4], kn;
BUF_MEM *buf;
EVP_MD_CTX *md_ctx = EVP_MD_CTX_new();
if (md_ctx == NULL) {
SSLerr(SSL_F_TLS_CONSTRUCT_SERVER_KEY_EXCHANGE, ERR_R_MALLOC_FAILURE);
al = SSL_AD_INTERNAL_ERROR;
goto f_err;
}
//在處理Client Hello的時候,服務端已經選擇了對應的密碼學套件。這個套件里面就包含了密碼學的選擇。這一步是溝通密碼學套件的細節,自然信息要基於選擇的密碼學套件
type = s->s3->tmp.new_cipher->algorithm_mkey;
//前面也有看到init_buf是用來構造待發送數據包的緩存
buf = s->init_buf;
//r是四個大數,OpenSSL密碼學運算的核心是大數系統。大數是指位數遠超過CPU能一個指令處理的數據長度。例如1024位長度的大數
r[0] = r[1] = r[2] = r[3] = NULL;
n = 0;
//處理DH算法的密碼學參數計算,從判斷中可以看到,這里包含了DHE和DHEPSK兩種
#ifndef OPENSSL_NO_DH
if (type & (SSL_kDHE | SSL_kDHEPSK)) {
CERT *cert = s->cert;
EVP_PKEY *pkdhp = NULL;
//DH結構體是用於存放DH算法的核心結構體。里面包含了DH算法用的p,g,q等參數,還有算法計算過程的中間結果
DH *dh;
//由於DH算法使用的數字要求是一個大素數,OpenSSL提供了一種動態產生大素數的方法,就是下文的ssl_get_auto_dh函數。但是也提供了允許用戶直接提供這個大素數的方法,也就是在dh_tmp_auto判斷不通過的情況下。我們要理解清楚的是,DH結構體代表的是一個數學層面的DH算法的計算,而EVP_PKEY代表的是一個非對稱加密的密碼學計算的上下文。DH是EVP_PKEY的一個參數。所以后續有一步EVP_PKEY_assign_DH(pkdh, dhp);的操作來完成這個密碼學計算上下文的設置。
if (s->cert->dh_tmp_auto) {
DH *dhp = ssl_get_auto_dh(s);
pkdh = EVP_PKEY_new();
if (pkdh == NULL || dhp == NULL) {
DH_free(dhp);
al = SSL_AD_INTERNAL_ERROR;
SSLerr(SSL_F_TLS_CONSTRUCT_SERVER_KEY_EXCHANGE,
ERR_R_INTERNAL_ERROR);
goto f_err;
}
EVP_PKEY_assign_DH(pkdh, dhp);
pkdhp = pkdh;
} else {
//如果不是動態生成,這個DH上下文就可以直接使用證書結構體里面的dh_tmp。雖然是存儲在證書結構體里面,但是這個參數並不是證書的一部分。而是使用者調用OpenSSL的API在創建上下文的時候動態創建的。使用的是SSL_CTRL_SET_TMP_DH的ctrl選項。
pkdhp = cert->dh_tmp;
}
//還有一種情況是既不是動態生成,用戶又沒有提供,而是用戶提供了用於生成大素數的回調函數。就調用這個回調函數來生成大素數。
if ((pkdhp == NULL) && (s->cert->dh_tmp_cb != NULL)) {
DH *dhp = s->cert->dh_tmp_cb(s, 0, 1024);
pkdh = ssl_dh_to_pkey(dhp);
if (pkdh == NULL) {
al = SSL_AD_INTERNAL_ERROR;
SSLerr(SSL_F_TLS_CONSTRUCT_SERVER_KEY_EXCHANGE,
ERR_R_INTERNAL_ERROR);
goto f_err;
}
pkdhp = pkdh;
}
if (pkdhp == NULL) {
al = SSL_AD_HANDSHAKE_FAILURE;
SSLerr(SSL_F_TLS_CONSTRUCT_SERVER_KEY_EXCHANGE,
SSL_R_MISSING_TMP_DH_KEY);
goto f_err;
}
//ssl_security是用來檢查當前的密碼學套件的安全等級的。任何的加密算法都有一個安全等級的概念。典型的就是私鑰的長度太小等。在OpenSSL中,不同的加密算法的不同參數都被賦予了不同的安全等級,一個OpenSSL運行的時候是不允許低於約定等級的密碼學算法被使用的。
if (!ssl_security(s, SSL_SECOP_TMP_DH,
EVP_PKEY_security_bits(pkdhp), 0, pkdhp)) {
al = SSL_AD_HANDSHAKE_FAILURE;
SSLerr(SSL_F_TLS_CONSTRUCT_SERVER_KEY_EXCHANGE,
SSL_R_DH_KEY_TOO_SMALL);
goto f_err;
}
if (s->s3->tmp.pkey != NULL) {
SSLerr(SSL_F_TLS_CONSTRUCT_SERVER_KEY_EXCHANGE,
ERR_R_INTERNAL_ERROR);
goto err;
}
//pkey域是用來存放每次握手動態生成的DH/ECDH的公鑰私鑰對的。是通過這里生成的DH參數來生成對應的公鑰私鑰對。這個邏輯不是程序規定的,而是DH算法就是這樣運行的。
s->s3->tmp.pkey = ssl_generate_pkey(pkdhp);
if (s->s3->tmp.pkey == NULL) {
SSLerr(SSL_F_TLS_CONSTRUCT_SERVER_KEY_EXCHANGE, ERR_R_EVP_LIB);
goto err;
}
//最后就是把DH算法中的p,q,g和生成的公鑰私鑰提取到函數的局部變量中,以待后續的使用
dh = EVP_PKEY_get0_DH(s->s3->tmp.pkey);
EVP_PKEY_free(pkdh);
pkdh = NULL;
DH_get0_pqg(dh, &r[0], NULL, &r[1]);
DH_get0_key(dh, &r[2], NULL);
} else
#endif
#ifndef OPENSSL_NO_EC
//橢圓曲線與DH並不沖突。DH是一種密鑰交換算法,橢圓曲線是一種計算方法。他們可以共同組合成ECDHE這種密碼交換算法。單獨的DHE就是用的DH的計算算法,DH的密鑰交換方法來完成密鑰交換。
if (type & (SSL_kECDHE | SSL_kECDHEPSK)) {
int nid;
if (s->s3->tmp.pkey != NULL) {
SSLerr(SSL_F_TLS_CONSTRUCT_SERVER_KEY_EXCHANGE,
ERR_R_INTERNAL_ERROR);
goto err;
}
/*nid是OpenSSL中用來組織各種各樣密碼學參數甚至密碼學細節選項的一種身份標記,是一個非常龐大的數據庫。一個OpenSSL上下文運行之后,當使用EC算法的時候,所使用的橢圓曲線的參數就是固定的了(運行之前可以配置)。例如是使用哪一條曲線,例如p-256曲線*/
nid = tls1_shared_curve(s, -2);
curve_id = tls1_ec_nid2curve_id(nid);
if (curve_id == 0) {
SSLerr(SSL_F_TLS_CONSTRUCT_SERVER_KEY_EXCHANGE,
SSL_R_UNSUPPORTED_ELLIPTIC_CURVE);
goto err;
}
//選擇了一條曲線之后,就是像DH一樣開始生成臨時的算法所需要的臨時參數
s->s3->tmp.pkey = ssl_generate_pkey_curve(curve_id);
if (s->s3->tmp.pkey == NULL) {
al = SSL_AD_INTERNAL_ERROR;
SSLerr(SSL_F_TLS_CONSTRUCT_SERVER_KEY_EXCHANGE, ERR_R_EVP_LIB);
goto f_err;
}
encodedlen = EVP_PKEY_get1_tls_encodedpoint(s->s3->tmp.pkey,
&encodedPoint);
if (encodedlen == 0) {
SSLerr(SSL_F_TLS_CONSTRUCT_SERVER_KEY_EXCHANGE, ERR_R_EC_LIB);
goto err;
}
n += 4 + encodedlen;
/*
* 在DH的代碼模塊最終提取了r對應的四個大數,但是在橢圓曲線,這四大大數會暫時置空
*/
r[0] = NULL;
r[1] = NULL;
r[2] = NULL;
r[3] = NULL;
} else
#endif /* !OPENSSL_NO_EC */
{
al = SSL_AD_HANDSHAKE_FAILURE;
SSLerr(SSL_F_TLS_CONSTRUCT_SERVER_KEY_EXCHANGE,
SSL_R_UNKNOWN_KEY_EXCHANGE_TYPE);
goto f_err;
}
for (i = 0; i < 4 && r[i] != NULL; i++) {
nr[i] = BN_num_bytes(r[i]);
#ifndef OPENSSL_NO_SRP
if ((i == 2) && (type & SSL_kSRP))
n += 1 + nr[i];
else
#endif
#ifndef OPENSSL_NO_DH
/*-
* 這一步是為了兼容windows的一些特點
*/
if ((i == 2) && (type & (SSL_kDHE | SSL_kDHEPSK)))
n += 2 + nr[0];
else
#endif
n += 2 + nr[i];
}
//加密都會有簽名算法。無論是非對稱還是對稱的工程使用過程,簽名算法都是不會缺少的
if (!(s->s3->tmp.new_cipher->algorithm_auth & (SSL_aNULL | SSL_aSRP))
&& !(s->s3->tmp.new_cipher->algorithm_mkey & SSL_PSK)) {
if ((pkey = ssl_get_sign_pkey(s, s->s3->tmp.new_cipher, &md))
== NULL) {
al = SSL_AD_DECODE_ERROR;
goto f_err;
}
kn = EVP_PKEY_size(pkey);
/* Allow space for signature algorithm */
if (SSL_USE_SIGALGS(s))
kn += 2;
/* Allow space for signature length */
kn += 2;
} else {
pkey = NULL;
kn = 0;
}
if (!BUF_MEM_grow_clean(buf, n + SSL_HM_HEADER_LENGTH(s) + kn)) {
SSLerr(SSL_F_TLS_CONSTRUCT_SERVER_KEY_EXCHANGE, ERR_LIB_BUF);
goto err;
}
d = p = ssl_handshake_start(s);
//之前有把各種算法計算得到的結果提取到r,這里就開始處理,主要是將這幾個數據轉化為網絡數據用於發送出去。這幾個數據就是服務器要使用Server Key Exchange發送給客戶端的數據
for (i = 0; i < 4 && r[i] != NULL; i++) {
#ifndef OPENSSL_NO_DH
/*-
* for interoperability with some versions of the Microsoft TLS
* stack, we need to zero pad the DHE pub key to the same length
* as the prime
*/
if ((i == 2) && (type & (SSL_kDHE | SSL_kDHEPSK))) {
s2n(nr[0], p);
for (j = 0; j < (nr[0] - nr[2]); ++j) {
*p = 0;
++p;
}
} else
#endif
s2n(nr[i], p);
BN_bn2bin(r[i], p);
p += nr[i];
}
#ifndef OPENSSL_NO_EC
if (type & (SSL_kECDHE | SSL_kECDHEPSK)) {
/*
如果使用了橢圓曲線,OpenSSL是只支持命名曲線的。例如目前應用最廣泛的p-256曲線,該曲線由於NIST的背景,不太被很多機構信任。谷歌目前在推廣X25519。所以這里寫入的密碼學參數的方法也是寫入了命名的曲線,OpenSSL也支持使用命名曲線。曲線類型,曲線數據長度和曲線的參數詳細信息被順序的寫入數據包
*/
*p = NAMED_CURVE_TYPE;
p += 1;
*p = 0;
p += 1;
*p = curve_id;
p += 1;
*p = encodedlen;
p += 1;
memcpy(p, encodedPoint, encodedlen);
OPENSSL_free(encodedPoint);
encodedPoint = NULL;
p += encodedlen;
}
#endif
/* pkey如果存在就證明服務器是有一個證書的,這在大部分情況下都是成立的。服務器一般會配置一個證書*/
if (pkey != NULL) {
//哈希算法無處不在,完整性校驗,確保不被改動,是安全的協議的前提。OpenSSL里面經常看到SIGALG這種簡寫,就是指的簽名算法。服務器會在這里選擇使用什么哈希算法來進行哈希計算。計算的內容包含了客戶端,服務端生成的隨機數還有服務端生成的密碼學參數
if (md) {
if (SSL_USE_SIGALGS(s)) {
if (!tls12_get_sigandhash(p, pkey, md)) {
al = SSL_AD_INTERNAL_ERROR;
SSLerr(SSL_F_TLS_CONSTRUCT_SERVER_KEY_EXCHANGE,
ERR_R_INTERNAL_ERROR);
goto f_err;
}
p += 2;
}
#ifdef SSL_DEBUG
fprintf(stderr, "Using hash %s\n", EVP_MD_name(md));
#endif
if (EVP_SignInit_ex(md_ctx, md, NULL) <= 0
|| EVP_SignUpdate(md_ctx, &(s->s3->client_random[0]),
SSL3_RANDOM_SIZE) <= 0
|| EVP_SignUpdate(md_ctx, &(s->s3->server_random[0]),
SSL3_RANDOM_SIZE) <= 0
|| EVP_SignUpdate(md_ctx, d, n) <= 0
|| EVP_SignFinal(md_ctx, &(p[2]),
(unsigned int *)&i, pkey) <= 0) {
SSLerr(SSL_F_TLS_CONSTRUCT_SERVER_KEY_EXCHANGE, ERR_LIB_EVP);
al = SSL_AD_INTERNAL_ERROR;
goto f_err;
}
s2n(i, p);
n += i + 2;
if (SSL_USE_SIGALGS(s))
n += 2;
} else {
/* Is this error check actually needed? */
al = SSL_AD_HANDSHAKE_FAILURE;
SSLerr(SSL_F_TLS_CONSTRUCT_SERVER_KEY_EXCHANGE,
SSL_R_UNKNOWN_PKEY_TYPE);
goto f_err;
}
}
if (!ssl_set_handshake_header(s, SSL3_MT_SERVER_KEY_EXCHANGE, n)) {
al = SSL_AD_HANDSHAKE_FAILURE;
SSLerr(SSL_F_TLS_CONSTRUCT_SERVER_KEY_EXCHANGE, ERR_R_INTERNAL_ERROR);
goto f_err;
}
EVP_MD_CTX_free(md_ctx);
return 1;
f_err:
ssl3_send_alert(s, SSL3_AL_FATAL, al);
err:
#ifndef OPENSSL_NO_DH
EVP_PKEY_free(pkdh);
#endif
#ifndef OPENSSL_NO_EC
OPENSSL_free(encodedPoint);
#endif
EVP_MD_CTX_free(md_ctx);
ossl_statem_set_error(s);
return 0;
}
OpenSSL在發送Servert Key Exchange的過程就是一個按照協商的密碼學套件,生成密碼學參數,然后寫入數據包的過程。這里有一個很重要的哈希操作,將本次通信用到的密碼學上下文涉及到的關鍵密碼學參數都進行了哈希。確保信道不會被篡改。
如果抓包的話也會看到這段哈希的結果。單純的看抓包的結果很難理解這段哈希的意義。實際上,這段哈希就是對整個信道描述的哈希。如果不是哈希,而是直接傳輸參與哈希的幾個參數,就能直接還原出這個信道。
TLS 1.2里面最神奇的一步就是Change Cipher。這一步不屬於握手的流程數據包,但是代表了一個程序內部的狀態。我們看到OpenSSL雖然產生了密碼學的上下文,但是至此也並沒有在本地進行一個密碼學上下文的構造。而按照標准,這個時候服務端就應該要做這件事情了。
OpenSSL里有兩個文件是根握手的過程相關性很高的。一個是s3_lib.c,一個是t1_lib.c。從名字上看,s3和t1分別指的是不同的TLS版本的庫函數的實現,但是實際上並不是如此。雙方提供的功能更多的是互補的。用來區別SSL和TLS不同版本的實現是在s3_enc.c和t1_enc.c中的。
在t1_lib.c中,我們能看到剛才的構造Server Hello擴展的時候使用的ssl_add_serverhello_tlsext函數。在處理客戶端Client Hello時候用到的解析擴展的ssl_parse_clienthello_tlsext函數。在生成密碼學參數的哈希鍵值的時候,我們要用到一個被提前設置的哈希,這個設置哈希的函數是tls1_set_server_sigalgs,也位於t1_lib.c。
這些擴展處理位於t1_lib.c,而通用的功能的處理就位於s3_lib.c了。這下就可以想明白這樣命名的具體含義了。t1_lib.c中存放的是與s3_lib.c中互補的握手用到的庫函數,但是這些函數所對應的功能都是在TLS 1.0之后才有的。
一個握手過程的庫函數大部分在SSLv3時代就已經成型了。例如對密碼學套件列表進行排序的函數ssl_sort_cipher_list,往數據包寫入數據的函數ssl3_handshake_write,從客戶端提供的密碼學套件列表中選擇密碼學套件的函數ssl3_choose_cipher,填充隨機數的函數ssl_fill_hello_random等等。雖然在新的版本與SSLv3的內容會不一樣,但是新版本的新內容在已有函數的情況下,都會選擇在s3_lib.c中直接修改添加。所以你也會在s3_lib.c中看到很多TLS 1.2相關的內容。OpenSSL的歷史包袱可見一斑。
握手的過程中,最重要的函數大類是密碼學相關的函數。如何通過非對稱加密協商得到的數據來生成對稱加密的信道,才是最麻煩的事情。這一部分的函數都是位於s3_enc.c和t1_enc.c中。這兩個文件的函數名都幾乎是一樣的,就是不同協議版本的同一個協議過程的不同的實現方法。


可以看到兩個文件的最大的一個區別就是TLS是擁有PRF算法的,而SSL3沒有。可以看到這兩個文件的核心思想是幾個密碼學的關鍵字:key block,finish mac, master secret。
ssl3_generate_key_block的代碼比較長,而TLS的代碼很簡單。
static int tls1_generate_key_block(SSL *s, unsigned char *km, int num)
{
int ret;
ret = tls1_PRF(s,
TLS_MD_KEY_EXPANSION_CONST,
TLS_MD_KEY_EXPANSION_CONST_SIZE, s->s3->server_random,
SSL3_RANDOM_SIZE, s->s3->client_random, SSL3_RANDOM_SIZE,
NULL, 0, NULL, 0, s->session->master_key,
s->session->master_key_length, km, num);
return ret;
}
因為TLS用了PRF算法。我們看PRF算法的輸入就知道如何產生這個Key Block。有客戶端和服務器的隨機數,然后是Master Key(Master Key和Master Secret指的是一個東西)。km是結果的存儲地址,num是結果的長度。
所以問題的關鍵就變成Master Key的意義是什么。同一個文件下的tls1_generate_master_secret函數就是用來生成Master Key的。因為服務端只有在收到了Client 的Key Exchange消息之后才有可能進行對稱加密上下文的生成。所以雖然服務端會先發送Change Cipher的消息到客戶端,但是實際的對稱加密上下文也還是要等到Client的消息發送完才會有。
OpenSSL中在握手的過程中收到的和接收的所有的數據包都會調用ssl3_finish_mac函數,這個函數如下:
int ssl3_finish_mac(SSL *s, const unsigned char *buf, int len)
{
if (s->s3->handshake_dgst == NULL)
return BIO_write(s->s3->handshake_buffer, (void *)buf, len) == len;
else
return EVP_DigestUpdate(s->s3->handshake_dgst, buf, len);
}
這個函數的作用是將握手過程的數據包寫到s->s3->handshake_buffer中。每一個讀寫的數據包最后都要參加最終的哈希計算,要確保數據包沒有被修改。這個寫入的數據包還有一個非常重要的功能就是在TLS版本的握手的時候用來生成key(SSLv3的時候不一樣)。
這里TLS是直接調用了SSLv3時代的函數ssl3_digest_cached_records來生成Master Key,而SSLv3的時候卻是有另外的計算方案,這個函數只是用來做整個握手的完整性校驗。TLS采用這種方案肯定是在發展的過程中發現了什么。無論是是SSLv3還是TLS,產生這個最重要的中間參數的函數入口都是ssl_generate_master_secret函數,這個函數位於ssl3_lib.c中。從這個函數與文件的從屬關系中,可以慢慢地體會到OpenSSL發展的過程中的一系列的變化。所以看OpenSSL要用發展的眼光去看,而不是用一個靜態的架構層面的概念去審視。從發展的層面看,作為一個發展了二十年的軟件,OpenSSL能夠如此,已經是非常的不容易。這個生成Master Key的函數由於在不同的版本中是不同的選擇,所以也就肯定存在一個方法表。
typedef struct ssl3_enc_method {
int (*enc) (SSL *, SSL3_RECORD *, unsigned int, int);
int (*mac) (SSL *, SSL3_RECORD *, unsigned char *, int);
int (*setup_key_block) (SSL *);
int (*generate_master_secret) (SSL *, unsigned char *, unsigned char *, int);
int (*change_cipher_state) (SSL *, int);
int (*final_finish_mac) (SSL *, const char *, int, unsigned char *);
int finish_mac_length;
const char *client_finished_label;
int client_finished_label_len;
const char *server_finished_label;
int server_finished_label_len;
int (*alert_value) (int);
int (*export_keying_material) (SSL *, unsigned char *, size_t,
const char *, size_t,
const unsigned char *, size_t,
int use_context);
uint32_t enc_flags;
unsigned int hhlen;
int (*set_handshake_header) (SSL *s, int type, unsigned long len);
int (*do_write) (SSL *s);
} SSL3_ENC_METHOD;
這個方法表中的大部分函數我們都看到過了。有的是通用的外層函數,大部分的都是分別位於s3_enc.c和t1_enc.c中的版本相關的函數。這就是不同協議的握手過程的方法表的封裝。從中,我們看到的目前遇到的兩個關鍵的值的函數,一個是Master Key,一個是Key Block,Key Block又是從Master Key中生成的。接下來Master Key的生成方法會非常讓人吃驚。
int tls1_generate_master_secret(SSL *s, unsigned char *out, unsigned char *p,
int len)
{
// 首先檢查是否支持擴展的Master Key(簡稱是EXTMS)。是否支持是EXTMS是由用戶決定的,用戶在發送Client Hello的時候有一個TLS擴展就叫做extended_master_secret擴展。如果用戶發送了這個擴展,后續服務端就都會使用這個擴展定義的方法來生成Master Key。現代的瀏覽器一般會啟用這個擴展。
if (s->session->flags & SSL_SESS_FLAG_EXTMS) {
unsigned char hash[EVP_MAX_MD_SIZE * 2];
int hashlen;
//對已經收到和發送的所有握手數據包進行摘要計算,得到一個哈希的結果
if (!ssl3_digest_cached_records(s, 1))
return -1;
//這一步是一個簡單的把摘要計算的結果拷貝出來
hashlen = ssl_handshake_hash(s, hash, sizeof(hash));
//然后直接通過PRF算法,輸入這個哈希的結果,來生成Master Key
tls1_PRF(s,
TLS_MD_EXTENDED_MASTER_SECRET_CONST,
TLS_MD_EXTENDED_MASTER_SECRET_CONST_SIZE,
hash, hashlen,
NULL, 0,
NULL, 0,
NULL, 0, p, len, s->session->master_key,
SSL3_MASTER_SECRET_SIZE);
OPENSSL_cleanse(hash, hashlen);
} else {
//如果沒有EXTMS,會直接使用雙方生成的隨機數來產生Master Key
tls1_PRF(s,
TLS_MD_MASTER_SECRET_CONST,
TLS_MD_MASTER_SECRET_CONST_SIZE,
s->s3->client_random, SSL3_RANDOM_SIZE,
NULL, 0,
s->s3->server_random, SSL3_RANDOM_SIZE,
NULL, 0, p, len, s->session->master_key,
SSL3_MASTER_SECRET_SIZE);
}
return (SSL3_MASTER_SECRET_SIZE);
}
這里面我們能看到兩個非常重要的結論。一個是EXTMS和普通MS(Master Secret,或者Master Key)的區別在於是使用雙方的隨機數還是使用握手流程的數據包進行PRF計算。但是有一點是相同的,就是產生Master Key的過程僅僅依賴於隨機數或者說是雙方產生的隨機數和一個p參數,這個p參數就是PRF中至關重要的安全參數。由於Key Block也是根據雙方的隨機數和Master Key產生的,所以Key Block也同樣的只是取決於產生的隨機數和整個數據包的流程和p參數,如果客戶端沒有啟用EXTMS,Key Block甚至只是由隨機數和p參數直接決定。而對稱加密所需要的所有密碼學參數都是從Key Block中獲得的,Key Block是對稱加密的信道充分描述。所以可以看到服務端和客戶端生成的隨機數在整個握手中的重要程度。也能看出來為什么要有EXTMS,如果沒有EXTMS,雙方的握手過程只要被監聽,或者只要隨機數引擎不健壯,或者是p參數出現了問題,信道就能被破解。
所以最關鍵的問題就是p參數的健壯性。我們能看到至此為止,握手密碼學參數交換的過程都沒有參與到最后的Master Key的計算,只是有一個p,而我們知道這是不可能的。所以p必然是和密碼學參數交換的過程的結果是重度相關的。
用來獲得這個p的結果的函數是EVP_PKEY_derive。
int EVP_PKEY_derive(EVP_PKEY_CTX *ctx, unsigned char *key, size_t *pkeylen)
{
if (!ctx || !ctx->pmeth || !ctx->pmeth->derive) {
EVPerr(EVP_F_EVP_PKEY_DERIVE,
EVP_R_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE);
return -2;
}
if (ctx->operation != EVP_PKEY_OP_DERIVE) {
EVPerr(EVP_F_EVP_PKEY_DERIVE, EVP_R_OPERATON_NOT_INITIALIZED);
return -1;
}
M_check_autoarg(ctx, key, pkeylen, EVP_F_EVP_PKEY_DERIVE)
return ctx->pmeth->derive(ctx, key, pkeylen);
}
我們不惜要了解太多的密碼學相關的知識,就能容易的猜到這個EVP_PKEY開頭的非對稱加密的derive操作就是拿到非對稱加密協商得到的一致結果的那個操作。也就是雙方非對稱握手過程中產生的共同的私鑰的結果。事實上也確實如此,例如對於DH算法,這一步的derive對應的就是pkey_dh_derive,實際上調用的DH_compute_key,就是一個計算非對稱握手的最終結果的一個過程。
我們審視整個密碼學參數的流程。非對稱加密握手的過程會對應一個得到一個密鑰的結果,得到的這個密鑰就是p,也可以叫做PMS(Pre Master Secret)。PMS這個概念在RFC中有定義的,只是不那么具體。RFC中說Master Secret是由PMS生成的。我們在代碼邏輯中也能看到,實際的TLS分為兩種情況(SSLv3又是另外一種,不作解釋),一種是EXTMS,一種是普通MS,EXTMS中使用了PMS,雙方生成的隨機數和整個握手過程的數據包來生成Master Key,普通MS比EXTMS少了一個握手過程的數據包參與計算。
生成的Master Key與兩個隨機數一起參與計算Key Block。然后由Key Block獲得對稱加密的三種信道參數。最后這一步比較難理解。Key Block是使用PRF算法生成的一個序列串。由於不同的加密套件對應的例如密鑰長度是不同的,所以也就決定了不同的密碼學套件的協商結果對應的Key Block長度是不同的。得益於PRF的得到任意長度序列串的能力,只要通信的雙方參與計算Key Block的值是相同的,那么計算得到的結果就是相同,而無論Key Block選擇是多長。
Key Block由三部分組成,一共6個參數。作為一個服務端,從第一個到第六個分別是讀取用到的哈希密鑰,寫入用到的哈希密鑰,讀取用到的對稱加密的密鑰,寫入用到的對稱加密的密鑰,讀取用到的IV值,寫入用到的IV值。對於客戶端來說,與服務端對應的反過來就可以。讀取的密鑰就是服務端寫入用到的密鑰。對於不同的加密套件,有的值甚至都可以直接的不存在。例如哈希密鑰在現代的GCM算法中是不存在的,也就是說長度為0,實際上GCM的Key Block只有4個密鑰。但是早期的AEAD算法,哈希密鑰就是存在的,因為它需要單獨的組合哈希算法和對稱加密算法。之所以哈希算法需要密鑰也就是前面說過的HMAC機制,就是帶密鑰的哈希。
這里還有一個點就是整個對稱加密的通信過程的兩個方向使用的密鑰是不同的。也就是說服務器往客戶端發送使用的密鑰和客戶端往服務器發送用到的密鑰是不同的。這個不同是雙向通信的一個安全性的考慮。相當於兩個對稱加密在同時工作,大大提高了信道的安全性,對暴力破解進一步免疫。
到這里,我們說完了信道的建立的過程,但是整個握手的過程並沒有說完。整個信道的建立過程還有重要的一步,常常被人誤解,也常常被人忽視。就是Change Cipher消息。Server在完成一次密碼學溝通(也就是Server Key Exchange)之后,是不發送Change Cipher Spec消息的,而如果Server 決定了復用一條連接的時候,Server 就會在Server Hello之后緊跟着回復Change Cipher Spec消息,而這個過程就是沒有Server Key Exchange消息的。也就是對於Server來說Server Key Exchange和Change Cipher Spec是一個二選一的過程。
這只是現象,而內部的原理就在於Change Cipher Spec的特殊語意上。Change Cipher Spec的實際的意義是解析Key Block生成對稱加密的信道。也就是說,這一個消息代表的是對稱加密信道的建立。問題就變成了服務端什么時候該建立這個對稱加密信道。一種是在收到客戶端的Client Hello Done之后,收到客戶端的所有握手數據包,握手就代表着完成,這個時候服務端才能夠生成Key Block。另外一種就是服務端決定復用連接的時候,因為服務端已經有了完整的密碼學上下文,這個時候只需要復原。復原操作就是Change Cipher Spec對應的操作,這個時候,服務端會發送Change Cipher Spec消息,並且對應的復原對稱加密的信道。
服務端在發送Server Key Exchange之前還有一步是發送證書,或者是可選的OCSP消息。發送的證書就是明文的配置的證書。除了存儲在文件的時候是Base64編碼的,發送的時候是明文的之外,內容上是沒有任何區別的。OCSP對應的消息是Certificate Status消息。只有在服務器開了這個選項之后才會發送。例如Nginx會在來請求的時候,按照指定的間隔去CA申請這個OCSP狀態,由於申請OCSP會比較耗時,所以Nginx如果開了OCSP的功能,會在OCSP過期的時候卡住一個連接。還是有一定的影響的。不過完全可以異步的線程在后台更新這個OCSP的信息,但是Nginx並不是這樣。
Server發送的最后一個消息Server Hello Done,沒有任何的內容,就是一個簡單的頭部,代表Server要發的東西都發送結束了。但是有的時候我們看到的Server最后一個數據包並不是Server Hello Done。如果抓包的話,能看到是一個Encrypted Handshaker Message,如下圖:
這種時候必然對應了沒有Server Key Exchange,也必然對應了有Server發送的Change Cipher Spec,因為前面也說過了,這種是Server采用了Session復用的技術。在復用的情況下,Server在回復Server Hello的時候就已經知道了全部的密碼學上下文。於是Server就立刻采用了。Server采用的標志就是Change Cipher Spec,按照語意,這個消息之后的所有數據都是加密的。所以Server接下來回復的消息看起來就是一個Encrypted Handshake Message,因為這條消息已經用復用的Session對稱加密的算法進行了加密。但是仔細觀察抓包能發現,這個包的長度並不是0,而Server Hello Done的數據包長度是0,如果這個數據包也是一個簡單的Server Hello Done,那么它的長度也應該是0,所以這里面也是必然有蹊蹺的。
case TLS_ST_SW_CHANGE:
st->hand_state = TLS_ST_SW_FINISHED;
return WRITE_TRAN_CONTINUE;
服務器的狀態機中可以看到,如果上一步是Change Cipher Spec,那么下一步將進入Finish消息,而不是Server Hello Done消息。也就是說,在復用Session的時候,服務端的最后一個消息並不是Server Hello Done,而是Finish Mac消息。
int tls_construct_finished(SSL *s, const char *sender, int slen)
{
unsigned char *p;
int i;
unsigned long l;
p = ssl_handshake_start(s);
i = s->method->ssl3_enc->final_finish_mac(s,sender, slen, s->s3->tmp.finish_md);
if (i <= 0)
return 0;
s->s3->tmp.finish_md_len = i;
memcpy(p, s->s3->tmp.finish_md, i);
l = i;
if (!s->server) {
OPENSSL_assert(i <= EVP_MAX_MD_SIZE);
memcpy(s->s3->previous_client_finished, s->s3->tmp.finish_md, i);
s->s3->previous_client_finished_len = i;
} else {
OPENSSL_assert(i <= EVP_MAX_MD_SIZE);
memcpy(s->s3->previous_server_finished, s->s3->tmp.finish_md, i);
s->s3->previous_server_finished_len = i;
}
if (!ssl_set_handshake_header(s, SSL3_MT_FINISHED, l)) {
SSLerr(SSL_F_TLS_CONSTRUCT_FINISHED, ERR_R_INTERNAL_ERROR);
return 0;
}
return 1;
}
可以清楚的看到Finish Mac這個包的內容是寫入了一個哈希結果,哈希結果實際是調用的s->method->ssl3_enc->final_finish_mac函數完成的。這個函數在方法表里面有看到,也是一個區分版本的哈希結果計算的函數。
int tls1_final_finish_mac(SSL *s, const char *str, int slen, unsigned char *out)
{
int hashlen;
unsigned char hash[EVP_MAX_MD_SIZE];
if (!ssl3_digest_cached_records(s, 0))
return 0;
hashlen = ssl_handshake_hash(s, hash, sizeof(hash));
if (hashlen == 0)
return 0;
if (!tls1_PRF(s, str, slen, hash, hashlen, NULL, 0, NULL, 0, NULL, 0,
s->session->master_key, s->session->master_key_length,
out, TLS1_FINISH_MAC_LENGTH))
return 0;
OPENSSL_cleanse(hash, hashlen);
return TLS1_FINISH_MAC_LENGTH;
}
在TLS中,這個函數是首先對目前為止的所有收發的數據包進行一次哈希計算,然后將哈希的結果和Master Key還有一個輸入的Label字符串,這個Label字符串默認是空的。實際上就是目前為止收發的數據包的哈希值和Master Key的PRF計算的結果。