我们目前大部分使用的openssl库还是基于TLS1.2协议的1.0.2版本系列,如果要支持更高的TLS1.3协议,就必须使用openssl的1.1.1版本或3.0版本。升级openssl库有可能会导致SSL会话失败,我在升级 wincurl 时,意外的收获了一个函数。这个函数非常的不起眼,但具有的现实意义却很大。

大部分情况下如果你不调用该函数,并不影响SSL会话和通信,但有时会被某些服务器拒绝。一旦被拒绝,查找具体的原因将变得非常痛苦。这个函数的意义好比HTTP协议中HOST字段,它和NGINX反向代理的Server name有异曲同工之妙。了解这个函数的意义,可能会让你今后在配置nginx反向代理时少走一些弯路。

这个函数是 SSL_set_tlsext_host_name,在介绍这个函数之前,我们先快速看看TLS协议和openssl的发展。

虽然TLS1.3的标准自2018年就已发布,但目前国内几乎所有网站并没有增加对TLS 1.3的支持,大部分主流网站使用的还是基于TLS1.2的版本。目前国内使用TLS1.3的好像只有知乎,其它诸如:新浪、网易、搜狐、腾讯、华为、京东、百度....都还是采用TLS1.2的标准。

 

网站是否支持TLS1.3其实并不重要,重要的是浏览器是否支持TLS1.3协议。好在目前市场上所有主流浏览器都已升级到了TLS1.3。因此对于用户而言,不管是支持TLS1.3的网站还是支持TLS1.2的网站,访问起来都不是问题,或者说对终端用户而言这种访问是无感的。

 

为何TLS3.0已经出现5年了,各大网络平台还是首选TLS1.2呢?估计还是出于对兼容和稳定的考虑。如果贸然升级到1.3导致大量客户端无法访问从而丢失用户,是任何平台所无法承受的。此外,支持TLS1.3的openssl版本也在不断完善中,在其未达到稳定前,最好还是使用稳定的1.2版本。这大概就是目前几乎所有平台类网站还在使用TLS1.2的原因。

 

此外,即使有的网站采用TLS1.3协议,也会继续提供对1.2的访问支持(向下兼容并保留所有TLS1.2的加密套件),除非该网站的运维人员强行使用TLS1.3的加密套件。但如果这样做的话就会导致大量仍然使用1.2标准的客户端程序无法继续访问该网站,这也意味着该运维人员离下岗再就业不远了。

 

这也是为什么我们即使不升级老版本的openssl库,也依然可以访问TLS1.3网站的原因。

 

上面说的TLS1.2,1.3指的是TLS的协议,实现上述协议的是openssl库。 其中支持TLS1.3的openssl库目前有两个版本系列:一个是1.1.1系列,一个是3.0系列。其中1.1.1更像是一个过渡版本,openssl团队承诺会支撑该版本到2023年9月11日,也就是明年911事件22周年之际(确实是一个值得纪念的日子)将停止更新1.1.1版本。这就意味着大家还没开始普及使用1.1.1,它就已经结束了。

这个其实也不重要,因为3.0才是最终openssl的终极版本,直接跳过了2.0,从而体现了openssl的跨越式发展。 3.0版本是openssl团队主推版本,也是一个长期维护的版本,该版本会维护到2026年9月7日。

虽然主流web浏览器都已支持TLS1.3,但浏览器只是客户端的一种,其它大部分客户端使用的还是openssl 1.0.2系列版本,甚至是1.0.1系列版本,也就是我们经常看到的libeay32.dllssleay32.dll 库,这些版本是不支持TLS1.3协议的。比如工具类的postman、curl,以及应用类QQ、Foxmail、微信以及各种下载客户端程序,使用的可能还是老的openssl库。openssl库在1.1.1及以上版本已经将库的导出名称改为libcryptolibssl,不过这只是前缀名,如果是1.1.1系列,则后缀为-1.1.dll,如果是3.0系列,则后缀为-3.dll,我们通过openssl的库名就能判断出该应用程序是否支持TLS1.3。

 

升级到TLS1.3可以从openssl的官网https://openssl.org)下载1.1.1或3.0版本的源码进行编译。编译完毕后,我们只要替换openssl库,并重新配置并编译你的客户端程序即可。

使用openssl库编写客户端程序的一般流程如下:先创建套接字,并连接到服务器,然后创建ssl,并绑定套接字,通过调动SSL_connect函数进行握手操作,成功后使用SSL_readSSL_write进行业务通信。

代码通常如下:

	// 创建套接字,并连接到服务器
SOCKET s = socket(AF_INET, SOCK_STREAM, 0);
struct hostent* hst = ::gethostbyname(pszServer);
if(NULL == hst)
return FALSE;
unsigned long addr;
struct sockaddr_in sockAddr;
memcpy(&addr, hst->h_addr, hst->h_length);
sockAddr.sin_family = AF_INET;
sockAddr.sin_port = htons(nPort);
sockAddr.sin_addr.S_un.S_addr = addr;
// 连接到服务器
if(SOCKET_ERROR != ::connect(s, (struct sockaddr *)&sockAddr, sizeof(struct sockaddr_in)))
return FALSE; // Openssl库初始化
OpenSSL_add_ssl_algorithms();
SSL_load_error_strings();
SSLeay_add_ssl_algorithms();
ERR_load_BIO_strings();
ctx = SSL_CTX_new(SSLv23_client_method()); // 创建ssl上下文,并绑定套接字
SSL* ssl = SSL_new (m_ctx);
SSL_set_fd(m_ssl, s);
// 开始ssl握手
int iRet = SSL_connect(m_ssl);
if(1 != iRet)
return FALSE; // 下面使用SSL_read或SSL_write进行通信

上述代码中SSL_connect函数是最重要的,它内部实现了SSL的握手过程,openssl在内部采用状态机的方式实现了整个握手,代码还是相当的晦涩。几乎所有的SSL通信失败都是在握手阶段产生的,比如加密套件不匹配,服务器证书没有可靠的签名等。

openssl中有个s_client命令集合,该命令用于实现客户端的通信,它的实现在s_client.c文件中,在这个庞大代码中有一个不起眼的函数调用,这个函数是SSL_set_tlsext_host_name。该函数的作用是在客户端在发送ClientHello消息时,将所访问的主机(服务器)名称写入到server name的扩展字段中。

   if (!noservername && (servername != NULL || dane_tlsa_domain == NULL)) {
if (servername == NULL) {
if(host == NULL || is_dNS_name(host))
servername = (host == NULL) ? "localhost" : host;
}
if (servername != NULL && !SSL_set_tlsext_host_name(con, servername)) {
BIO_printf(bio_err, "Unable to set TLS servername extension.\n");
ERR_print_errors(bio_err);
goto end;
}
}

调用该函数后会在ClientHello的扩展字段中增加一个Server Name字段。如下图所示:

我们在客户端编程时,通常不会调用该函数,因为会觉得该字段可有可无,既然已经连接到Host服务器上,为何还要将Host的名称告诉给服务器呢?如果我们不调用SSL_set_tlsext_host_name 函数,难道SSL会话会失败么?实际上即使不调用该函数,也是可以握手成功的,并不影响TLS的正常通信。

但如果服务器强制校验该字段时,就会导致握手失败,这就好比HTTP协议中请求的HOST字段一样。那么为什么有的服务器要强制校验该字段呢?如果一台服务器绑定不同的DSN名称(也就是一个IP地址绑定多个域名),这个扩展字段的意义就出来了,服务器会根据不同的域名提供不同的证书。

 

举个例子,假设BAT都破产了,他们穷到共用一台服务器的地步,也就是一个IP地址绑定了百度、阿里和腾讯三个公司域名,并且这三家公司都给域名申请了数字证书,此时扩展字段的HOST的作用就体现出来了,openssl服务器端会跟据不同的host名称决定返回哪家公司的数字证书。这也是Ngnix反向代理的原理,根据不同域名进行不同访问地址映射。

写到这里,本文也即将结束,也许我们调用了SSL_set_tlsext_host_name函数也无法知道它的最终目的是什么,这也是我写这篇文章的原因之一吧。

openssl客户端编程:一个不起眼的函数导致的SSL会话失败问题的更多相关文章

  1. 使用c语言实现在linux下的openssl客户端和服务器端编程

    使用c语言实现在linux下的openssl客户端和服务器端编程 摘自:https://www.cnblogs.com/etangyushan/p/3679457.html 前几天组长让我实现一个使用 ...

  2. [Real World Haskell翻译]第22章 扩展示例:Web客户端编程

    第22章 扩展示例:Web客户端编程 至此,您已经看到了如何与数据库交互,解析一些数据,以及处理错误.现在让我们更进了一步,引入Web客户端库的组合. 在本章,我们将开发一个真正的应用程序:一个播客下 ...

  3. UNIX网络编程——getsockname和getpeername函数

    UNIX网络编程--getsockname和getpeername函数   来源:网络转载   http://www.educity.cn/linux/1241293.html     这两个函数或者 ...

  4. JAX-RS 2.0 REST客户端编程实例

    JAX-RS 2.0 REST客户端编程实例 2014/01/28 | 分类: 基础技术, 教程 | 0 条评论 | 标签: JAX-RS, RESTFUL 分享到:3 本文由 ImportNew - ...

  5. linux网络编程九:splice函数,高效的零拷贝

    from:http://blog.csdn.net/jasonliuvip/article/details/22600569 linux网络编程九:splice函数,高效的零拷贝 最近在看<Li ...

  6. 在C语言中以编程的方式获取函数名

    仅仅为了获取函数名,就在函数体中嵌入硬编码的字符串,这种方法单调乏味还易导致错误,不如看一下怎样使用新的C99特性,在程序运行时获取函数名吧. 对象反射库.调试工具及代码分析器,经常会需要在运行时访问 ...

  7. 基于X.509证书和SSL协议的身份认证过程实现(OpenSSL可以自己产生证书,有TCP通过SSL进行实际安全通讯的实际编程代码)good

    上周帮一个童鞋做一个数字认证的实验,要求是编程实现一个基于X.509证书认证的过程,唉!可怜我那点薄弱的计算机网络安全的知识啊!只得恶补一下了. 首先来看看什么是X.509.所谓X.509其实是一种非 ...

  8. Dynamics 365客户端编程示例:两个选项集字段的联动

    我是微软Dynamics 365 & Power Platform方面的工程师罗勇,也是2015年7月到2018年6月连续三年Dynamics CRM/Business Solutions方面 ...

  9. Python全栈开发之3、深浅拷贝、变量和函数、递归、函数式编程、内置函数

    一.深浅拷贝 1.数字和字符串 对于 数字 和 字符串 而言,赋值.浅拷贝和深拷贝无意义,因为其永远指向同一个内存地址. import copy # 定义变量 数字.字符串 # n1 = 123 n1 ...

随机推荐

  1. redis无损数据迁移

    在dba眼中,redis仅仅是一个缓存,不适合作为存储来使用,不管是redis-sentinel集群还是cluster集群,在redis主节点发生意外宕机时没有机制来保证主从节点数据的一致性.但是,很 ...

  2. JS DOM简介

    1. 概述 文档对象模型(Document Object Model,简称 DOM),是W3C组织推荐的处理可扩展标记语言(HTML或者XML)的标准编程接口.W3C 已经定义了一系列的 DOM 接口 ...

  3. Oracle中查找某个点半径范围内的所有经纬度(优化)

    需求: 已知一个点的经纬度,需要从表中找出以这个点为中心,半径M米范围内的所有经纬度数据. 假设现有表 TAB_LONG_LAT_DATA,字段如下: ID                  INTE ...

  4. ucore lab8 文件系统 学习笔记

    最后一战果然过瘾.代码量够多,新机制够复杂度,都管饱.做这一课就像从高山上往下走,坡急且路险,还不知自己的方位,琢磨不透系统的架构.待到下了山,回头一看豁然开朗,原来方才自己所下的山是这般模样.在这里 ...

  5. Vert.X CompositeFuture 用法

    CompositeFuture 是一种特殊的 Future,它可以包装一个 Future 列表,从而让一组异步操作并行执行:然后协调这一组操作的结果,作为 CompositeFuture 的结果.本文 ...

  6. 五四青年节,今天要学习。汇总5道难度不高但可能遇到的JS手写编程题

    壹 ❀ 引 时间一晃,今天已是五一假期最后一天了,没有出门,没有太多惊喜与意外.今天五四青年节,脑子里突然想起鲁迅先生以及悲欢并不相通的话,我的五一经历了什么呢,忍不住想说那大概是,父母教育孩子大声嚷 ...

  7. numpy.core._exceptions.UFuncTypeError: ufunc 'subtract' did not contain a loop with signature matching types (dtype('<U1'), dtype('float64')) -> None

    在机器学习实战的Logistic回归梯度上升优化算法中遇到了这个问题 numpy.core._exceptions.UFuncTypeError: ufunc 'subtract' did not c ...

  8. 【算法】计数排序(Counting Sort)(八)

    计数排序(Counting Sort) 计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中. 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范 ...

  9. ASCII&Base64

    ASCII https://zh.wikipedia.org/wiki/ASCII American Standard Code for Information Interchange,美国信息交换标 ...

  10. 好客租房55-props深入(2props校验)

    对于组件来说 props是外来的 无法保证使用者传入什么格式的数据 传入的数据格式不对 可能会导致组件内部报错 关键问题:不知道报错的具体原因 1安装包props-types 2导入props-typ ...