浅析SSL/TLS的会话流程和源码实现
- 一、SSL/TLS的概念
- 二、SSL/TLS的会话交互流程
- (1) client_hello
- (2) server_hello + certificate + sever hello done
- (3) client key exchange + change cipher spec + encrypted handshake message
- (4) new session ticket+change cipher spec+envrypted handshake message
- 三、源码实现
一、SSL/TLS的概念
官方解释TLS叫做安全传输层协议(TLS)用于在两个通信应用程序之间提供保密性和数据完整性。SSL就是TLS的前身,从最初的SSL3到现在的TLS 1.3,其协议核心并没有大幅改变。只是为了解决几个安全性问题。TLS实际是在明文的上层和 TCP 层之间加上一层加密,这样就保证上层信息传输的安全,所以在网络模型中它运行于传输层之上,隶属于会话层。实际应用例如在浏览器访问中,在HTTP 协议中加上 SSL 层之后,就有了HTTPS。
二、SSL/TLS的会话交互流程
ssl实际应用中具体的交互流程如下图所示:
(1) client_hello
客户端发起协议交互请求,其中包含tls的版本信息,加密套件候选列表,随机数等相关信息,以我们的设备为例抓包来分析具体实况:
- tls的版本信息主要是当前客户端所支持的最高 TLS 协议版本 ,目前已有的TLS 协议版本总共有五种,从低到高依次 SSLv2, SSLv3, TLSv1, TLSv1.1, TLSv1.2
- 客户端所支持的加密套件cipher suites, 每个加密套件对应TLS 原理中的四个功能的组合:
- 认证算法 Au (身份验证)
- 密钥交换算法 KeyExchange (密钥协商)
- 对称加密算法 Enc (信息加密)
- 信息摘要 Mac (完整性校验)
- 随机数 random_C,用于后续的密钥的生成
(2) server_hello + certificate + sever hello done
- server_hello, 服务端返回协商的信息结果,包括选择使用的协议版本 version,选择的加密套件 cipher suite,选择的压缩算法 compression method、随机数 random_S 等,其中随机数用于后续的密钥协商
- server_certificates, 服务器端配置对应的证书链,用于身份验证与密钥交换
- server_hello_done,通知客户端 server_hello 信息发送结束
(3) client key exchange + change cipher spec + encrypted handshake message
- client_key_exchange: 合法性验证通过之后,客户端计算产生随机数字 pre-master,并用证书公钥加密,发送给服务器
- 此时客户端已经获取全部的计算协商密钥需要的信息:两个明文随机数 random_C 和 random_S 与自己计算产生的 pre-master,计算得到协商密钥enc_key=Fuc(random_C, random_S, pre-master)
- change_cipher_spec: 客户端通知服务器后续的通信都采用协商的通信密钥和加密算法进行加密通信;
- encrypted_handshake_message: 结合之前所有通信参数的 hash 值与其它相关信息生成一段数据,采用协商密钥 session secret 与算法进行加密,然后发送给服务器用于数据与握手验证
(4) new session ticket+change cipher spec+envrypted handshake message
- session ticket,SessionTicket是一种不需要服务器端状态的,恢复TLS session的方式。SessionTicket可以用于任何CipherSuite,服务器如果使用 SessionTicket 机制,服务器需要把本地的 session 状态存入一个ticket中,ticket会被加密,并被MAC保护,无法篡改,加密和算MAC用的key只有服务器知道。加密并MAC过的ticket用 NewSessionTicket 消息分发给客户端, 客户端把收到的ticket和master secret等其它与当前session有关的参数一起,缓存起来。当客户端希望恢复会话时,就把ticket包含在 ClientHello 的 SessionTicket 扩展中发给服务器。服务器收到后,解密ticket,算MAC确认ticket没有被篡改过,然后从解密的内容里面,获取session 状态,用来恢复会话。如果服务器成功地验证了ticket,可以在 ServerHello 之后返回一个 NewSessionTicket 消息来更新ticket。
- 服务器用私钥解密加密的 pre-master 数据,基于之前交换的两个明文随机数 random_C 和 random_S,计算得到协商密钥:enc_key=Fuc(random_C, random_S, pre-master); 计算之前所有接收信息的 hash 值,然后解密客户端发送的 encrypted_handshake_message,验证数据和密钥正确性;
- change_cipher_spec, 验证通过之后,服务器同样发送 change_cipher_spec 以告知客户端后续的通信都采用协商的密钥与算法进行加密通信;
- encrypted_handshake_message, 服务器也结合所有当前的通信参数信息生成一段数据并采用协商密钥 session secret 与算法加密并发送到客户端;
三、源码实现
这里针对ssl的交互流程,我们搭配openssl的库来实现一套具体的交互配置流程,前提是我们已经申请过了CA证书。
首先是ssl的参数CTX的创建,需要我们配置当前ssl的CA证书路径,验证链深度,加密套件等用于服务端的握手条件
SSL_CTX* config_ctx(HPR_VOID)
{
HPR_INT32 sdwRet = HPR_ERROR;
SSL_CTX *pCtx = NULL;
SECURE_ENCRY_PARAM stSecureEncryParam ;
pCtx = SSL_CTX_new(TLSv1_2_server_method());
if (NULL == pCtx)
{
ERR_print_errors_fp(stdout);
SSL_COM_ERR("SSL_CTX_new fail\n");
return NULL;
}
SSL_CTX_set_verify(pCtx, SSL_VERIFY_NONE, NULL); //不验证
/** 设置证书验证链的深度 默认深度为 9*/
SSL_CTX_set_verify_depth(pCtx, 9);
/** 设置使用或者需要禁止的安全套件*/
SSL_CTX_set_cipher_list(pCtx, "TLSv1:!LOW:!MEDIUM:!MD5:!ADH:!NULL:!DH");
/** 设置证书加密的密码*/
SSL_CTX_set_default_passwd_cb_userdata(pCtx, pwd);
if (ssl_ctx_use_certificate_file_v2(pCtx, path, SSL_FILETYPE_PEM) <= 0) //path为CA证书,即sever.crt
{
ERR_print_errors_fp(stderr);
SSL_COM_ERR("SSL_CTX_use_certificate_file fail\n",path);
goto EXIT_PORT;
}
if (ssl_ctx_use_privatekey_file_v2(pCtx, PRI_KEY_FILE, SSL_FILETYPE_PEM) <= 0) //PRI_KEY_FILE为证书私钥
{
ERR_print_errors_fp(stderr);
SSL_COM_ERR("ssl_ctx_use_privatekey_file_v2 %s fail\n", PRI_KEY_FILE);
goto EXIT_PORT;
}
// 判断使用的证书秘钥对是否匹配有效
if (!SSL_CTX_check_private_key(pCtx))
{
ERR_print_errors_fp(stderr);
SSL_COM_ERR("Private key does not match the certificate public key\n");
goto EXIT_PORT;
}
//安全地使用SSL_OP_ALL来启用错误解决方法选项
//SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION 在作为服务器执行重新协商时,始终启动新会话(即,仅在初始握手中接受会话恢复请求)。客户端不需要此选项。*/
SSL_CTX_set_options(pCtx, SSL_OP_ALL | SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION);
/* SSL_CTX_set_options(pCtx, SSL_OP_NO_RENEGOTIATION); // 解决安全扫描出现的 TLS Client-initiated 重协商攻击(CVE-2011-1473) 漏洞, 禁用TLSv1.2及更早版本中的所有重新协商。不发送HelloRequest消息,并通过ClientHello忽略重新协商请求。, OpenSSL 1.1.1 版本开始支持, 1.1 之前使用 pSsl->s3->flags |= SSL3_FLAGS_NO_RENEGOTIATE_CIPHERS配置 */
// 设置回掉函数,内部可以用来限制重协商的次数
SSL_CTX_set_info_callback(pCtx, ssl_info_callback);
SSL_CTX_set_mode(pCtx, SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_AUTO_RETRY);
return pCtx;
EXIT_PORT:
/* This will never occur! */
if(NULL != pCtx )
{
SSL_CTX_free(pCtx);
pCtx = NULL;
}
return NULL;
}
其次是将fd的绑定到我们之前已经创建的ctx上,前面已经说过tls是工作在tcp之上,当我们创建一个tcp连接之后需要将fd绑定到ssl
SSL* bind_ssl_fd(HPR_INT32 connfd, SSL_CTX *ctx)
{
SSL *pSsl = NULL;
HPR_INT32 sdwRet = -1;
if (NULL == ctx || -1 == connfd)
{
return NULL;
}
/* 基于 ctx 产生SSL*/
pSsl = SSL_new(ctx);
if (NULL == pSsl)
{
SSL_COM_ERR(" SSL_new fail \n");
return NULL;
}
/* 将连接用户的 socket 加入SSL*/
sdwRet = SSL_set_fd(pSsl, connfd);
if (sdwRet == 0)
{
SSL_COM_ERR(" SSL_set_fd fail ret =%d \n", sdwRet);
goto EXIT_PORT;
}
/** 该函数若设置为 true, ssl 将会尽可能多地读取数据到ssl缓冲区, 若设置为false 则将只读取当前处理的record*/
SSL_set_read_ahead(pSsl, false);
/* SSL_set_accept_state(pSsl); */
pSsl->s3->flags |= SSL3_FLAGS_NO_RENEGOTIATE_CIPHERS;
/*建立 SSL 连接*/
sdwRet = SSL_accept(pSsl);
if (sdwRet <= 0)
{
ERR_print_errors_fp(stderr);
SSL_COM_ERR("SSL_accept fail ret =%d \n", sdwRet);
goto EXIT_PORT;
}
SSL_COM_INFO("server: Connected with %s encryption\n", SSL_get_cipher(pSsl));
show_certs_info(pSsl);
return pSsl;
EXIT_PORT:
if(NULL != pSsl)
{
SSL_shutdown(pSsl);
SSL_free(pSsl);
pSsl = NULL;
}
return NULL;
}
这里我们的ssl连接已经创建完毕,当我们接受或者需要写入数据时调用ssl的读写接口就可以对数据进行加密传输了
HPR_INT32 ssl_readn(SSL *ssl, HPR_INT8* buf, HPR_INT32 len, HPR_INT32 *pdwSslStoreLen)
{
HPR_INT32 nLeft = len;
HPR_INT32 recvLen = 0;
HPR_INT32 dwSslStoreLen = 0;
HPR_INT32 connfd = -1;
HPR_INT32 err = -1;
struct timeval select_timeout;
fd_set rset;
if(NULL == ssl || NULL == buf )
{
SSL_COM_ERR("err Param \n");
return HPR_ERROR;
}
connfd = SSL_get_fd(ssl);
if (-1 == connfd)
{
SSL_COM_ERR("SSL_get_ssl fail\n");
return HPR_ERROR;
}
FD_ZERO(&rset);
FD_SET(connfd, &rset);
while (nLeft > 0)
{
select_timeout.tv_sec = 5;
select_timeout.tv_usec = 0;
/** @brief: select 仅可检测到链路层的可读数据, 对于已经缓存在ssl缓冲区但是未接收到应用层的数据 无法检测,所以用dwSslStoreLen进行储值判断*/
/** ssl 缓冲区没有数据才需要等待 select*/
if(0 == dwSslStoreLen)
{
if (select(connfd + 1, &rset, NULL, NULL, &select_timeout) <= 0)
{
SSL_COM_ERR("readn: select failed, errno = 0x%x\n", errnoGet());
return len - nLeft;
}
}
do
{
/** @note: 无论SSL_read多少字节的数据(即便是0),SSL都会讲一个完整的recode 读进ssl缓冲区,这个recode的数据将不再存在于tcp层,无法被select到, SSL_read 再从ssl缓冲区读进应用层的buf */
recvLen = SSL_read(ssl, buf + len - nLeft, nLeft);
if (recvLen > 0)
{
nLeft -= recvLen;
}
else
{
err = SSL_get_error(ssl, recvLen);
/** @brief: fd暂时无数据可读*/
if (SSL_ERROR_WANT_READ == err)
{
recvLen = 0;
SSL_COM_ERR("SSL_ERROR_WANT_READ, fd = %d , has recv %d len \n", connfd, len - nLeft);
}
/** @brief: 这里 ssl链路断开 就视为链路无效了,就退出链路*/
/** @brief: ssl链路断开, 注意: 这不一定代表 tcp 链路已经断开*/
else if (SSL_ERROR_ZERO_RETURN == err)
{
SSL_COM_ERR("SSL_ERROR_ZERO_RETURN, fd = %d, has recv %d len \n", connfd, len - nLeft);
return len - nLeft;
}
else if (SSL_ERROR_SYSCALL == err)
{
SSL_COM_ERR("SSL_ERROR_SYSCALL, fd = %d, has recv %d len \n", connfd, len - nLeft);
SSL_COM_ERR ("错误代码%d,错误信息是'%s'\n", errno, strerror(errno));
return len - nLeft;
}
else
{
SSL_COM_ERR("select exit, err = %d, fd = %d, has recv %d len \n",errno, connfd, len - nLeft);
return len - nLeft;
}
}
dwSslStoreLen = SSL_pending(ssl);
}while (nLeft > 0 && dwSslStoreLen); /** 需要读取的数据还没读完, 且ssl缓存区还有数据*/
}
if (NULL != pdwSslStoreLen)
{
*pdwSslStoreLen = dwSslStoreLen;
}
return len - nLeft;
}
HPR_INT32 ssl_writen(SSL *ssl, HPR_INT8* buf, HPR_INT32 len)
{
HPR_INT32 nLeft = len;
HPR_INT32 sendLen = 0;
HPR_INT32 err = 0;
HPR_INT32 connfd = -1;
if(NULL == ssl || NULL == buf )
{
SSL_COM_ERR("err Param \n");
return HPR_ERROR;
}
while (nLeft > 0)
{
sendLen = SSL_write(ssl, buf + len - nLeft, nLeft);
if (sendLen >= 0)
{
nLeft -= sendLen;
}
else
{
/** @brief: fd暂时不可写*/
err = SSL_get_error(ssl, sendLen);
if (SSL_ERROR_WANT_WRITE == err)
{
sendLen = 0;
SSL_COM_ERR("SSL_ERROR_WANT_WRITE\n");
}
/** @brief: ssl链路断开, 注意: 这不一定代表 tcp 链路已经断开*/
else if (SSL_ERROR_ZERO_RETURN == err)
{
SSL_COM_ERR("SSL_ERROR_ZERO_RETURN\n");
return len - nLeft;
}
else if (SSL_ERROR_SYSCALL == err)
{
SSL_COM_ERR("SSL_ERROR_SYSCALL\n");
SSL_COM_ERR("错误代码%d,错误信息是'%s'\n", errno, strerror(errno));
return len - nLeft;
}
else
{
SSL_COM_ERR("select exit, err = %d\n", err);
return len - nLeft;
}
}
}
return len - nLeft;
}
参考文档: https://blog.csdn.net/ustccw/article/details/76691248.