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 ", 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计算的结果。