浅析SSL/TLS的会话流程和源码实现

news/2024/7/5 0:53:49

浅析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实际应用中具体的交互流程如下图所示:
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.


http://www.niftyadmin.cn/n/3879832.html

相关文章

每天一道算法题(7)——在字符串中删除特定的字符

题目&#xff1a;输入两个字符串&#xff0c;从第一字符串中删除第二个字符串中所有的字符。例如&#xff0c;输入”They are students.”和”aeiou”&#xff0c;则删除之后的第一个字符串变成”Thy r stdnts.”。 1.思路 最简单的。设source长n&#xff0c;key 长m(n>>…

将文本藏入图片 选择自 VirleneCheng 的 Blog

一般看来文字与图片是毫不相同的&#xff0c;但是它们却有共同点。图片是由一个个点组成的&#xff0c;而这些点的颜色值可由数字组成&#xff0c;文字可由ASCII码表示&#xff0c;这就使得数字成为它们之间沟通道桥梁。因此就可以将文本藏入图片中。这可以用Visual Basic 6.0实…

创建组件“AxLicenseControl”失败

打开以前的程序&#xff0c;准备来添加一个功能&#xff0c;打开主程序就报错&#xff1a; 我未曾改变过版本&#xff0c;原来是由于破解测试需要&#xff0c;修改了系统时间&#xff0c;时间对不了&#xff0c;ArcGIS的问题&#xff0c;改过来就正常了。

在中文下打开日文文件:)

private void button1_Click(object sender, System.EventArgs e) { openFileDialog1.Filter "所有文件*.*|*.*|文本文件*.txt|*.txt"; openFileDialog1.FilterIndex 2; if(openFileDialog1.ShowDialog ()DialogResult.OK ) { FileStream frnew FileStre…

php数组函数序列之array_unshift() 在数组开头插入一个或多个元素

array_unshift() 函数在数组开头插入一个或多个元素。被加上的元素作为一个整体添加&#xff0c;这些元素在数组中的顺序和在参数中的顺序一样 array_unshift()定义和用法 array_unshift() 函数在数组开头插入一个或多个元素。 被加上的元素作为一个整体添加&#xff0c;这些元…

EFFECTIVE-C++读书笔记

读书笔记2. 构造/析构/赋值运算条款05&#xff1a; 了解C默默编写并调用了哪些函数条款06&#xff1a;若不想使用编译器自动生成的函数。就该明确拒绝条款07&#xff1a;为多态基类声明virtual析构函数条款08&#xff1a;别让异常逃离析构函数条款09&#xff1a;绝不在构造和析…

HDU_1166_敌兵布阵

敌兵布阵 Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others)Total Submission(s): 54389 Accepted Submission(s): 22819 Problem DescriptionC国的死对头A国这段时间正在进行军事演习&#xff0c;所以C国间谍头子Derek和他手下Tidy又开…

关于鼠标和键盘的全局获取的一个类 选择自 hbxtlhx 的 Blog

用这个类的方法Start可以开始捕获键盘和鼠标的在全局事件和相应的参数信息&#xff0c;也就所谓的钩子程序&#xff1a; 以前见一个高人写的一个程序&#xff0c;开始看不明白&#xff0c;经过我的&#xff02;反译&#xff02;变的好理解了些&#xff0c;拿来和大家共享一下&a…