1 写在前面
最近博主又遇到了一个非常头疼的网络问题,还是差点要 通宵加班 那种,所幸的是,在 deadline 之前有效地解决了问题。
不过,这次问题有点不一样,它不是简单的TCP网络问题,而是基于TLS的网络连接问题。
本文将会深度复盘本次的在网排查思路,通过本文的阅读,你将会了解到以下几部分的核心内容:
TLS握手的基本流程;
数字证书的作用及其基本格式内容;
如何使用工具查看X509格式的数字证书;
证书链的理论与实践;
网络抓包分析的方法;
如何高效地阅读和排查mbedtls的源码?
基于mbedtls实现的TLS,如何裁剪和配置?
mbedtls的LOG打印如何调试?
2 问题描述
2.1 项目背景
最近公司签了一个大客户的私有化部署项目,根据双方的约定,我们需要将我们的IoT方案部署在客户的私有环境上,以便于客户能够自己掌控整一个设备数据的权限与安全等相关信息。
为此,我们平台开发的同事还特意出差到客户现场,计划安排一周的时间,完成私有化部署、在网设备的接入验证,以及相关的技术培训等等内容。我们平台的测试同事还特意带了我们的测试设备过去,准备现场直接入网接入测试等一系列工作。
一开始平台侧的环境部署还是比较顺利的,把我们的各种服务部署到客户的服务器上,很快就把服务给跑起来了。
在我们的IoT方案中,服务端暴露的是一个 MQTT broker ,终端设备通过 MQTT 协议连接上broker,进而通过broker分发消息,实现业务消息的上下行。
整一个架构示意图类似这样:
2.2 现场问题
后端服务跑起来后,已经来到了第三天,开始做终端侧和移动端侧的连接验证,测试人员把之前带过去的终端设备跑起来,发现终端设备连不到现在私有化的后端环境。
由于测试的同事测的比较多,很快从日志中发现了,当前终端的固件默认链接的是以前公网部署环境,而不是客户私有化部署的环境。
于是,第一时间请求我们:终端的固件要增加对新的私有化部署环境进行支持,需要增改代码。
根据我们过往的需求开发经验,早前也有其他的客户执行过类似的私有化部署操作,我们终端侧改起来还是很快的,依葫芦画瓢,把对应的域名、URL和端口,还有生产使用的数字证书(因为我们的通讯链路走的是 MQTTS,即MQTT + TLS);全部给到我们之后,很快,我们就输出了一版新的测试固件,该固件理论上就可以支持对新的私有化部署环境的连接访问。
哪知,又出幺蛾子了:客户的部署环境的域名还未完成备案(据说还在等lingdao签字审批,流程比较复杂),所以终端无法对该域名做正确的解析,即便解析了,获取的IP地址也不是客户的环境,而是另一个不知道是哪的IP地址,反正就是网络数据包到不了后台环境。
好在当时部署的现场,运维的同事也在现场,紧急部署了一个路由器环境,发射一个特定的Wi-Fi热点以供测试的终端设备去连接,在这个路由器中,运维已经对它的域名解析(配置域名服务器和HOST信息)做了手脚,使其能够正确解析客户部署的域名环境,终端得以拿到正确的IP地址,从而能够访问到后端服务环境。
测试快速地使用新搭建的Wi-Fi热点进行新一轮测试,终于看到连接过去了,但是之前没有见过的 TLS握手失败 如期而至,自然问题一个劲的直接甩到我们这边。
看样子真的TLS握手失败,mbedtls返回的错误码是 -0x7a00,如下所示:
3 场景复现
3.1 过往经验
看到上面报过来的现场问题,说实话,我也是懵的,因为这个错误码在过往的调试、开发、测试过程中并没有见过。虽然之前也遇到过一些 TLS握手失败的问题,一般可能就是CA证书没有配好,或者还有一个 mbedtls配置项 MBEDTLS_SSL_MAX_CONTENT_LEN 没有配好,导致的。
这个配置项坑过我们几次,后面干脆我们把配置这个参数的菜单项都留出来了,我们看下它的定义(摘自RT-Thread中的 软件包mbedtls的配置项说明):
一般我们会在一些客户的现网环境下容易出现这个 0x7200 的错误,为了节省终端的RAM开销,我们默认使用的是 4096,而当有些后台环境的证书过大时(证书支持的项目内容较多或者多几级证书链)时,就会引发这个问题。
解决的方法也比较简单,把这个 MBEDTLS_SSL_MAX_CONTENT_LEN 配大一些,比如 8192 ,就可以解决这个问题。
根据这个过往经验,我们尝试把参数改大,再编译固件给现场做验证,问题依旧,证明并不会由这个配置项引发的。
无奈,既然过往的经验不顶用,自然得另寻他法了,不能一直这么卡着,研发的作用不正是 发现问题并解决问题 吗?换句话说,成长的路径不正是解决一个又一个的未知问题 吗?
3.2 搭建终端复现环境
虽说是要好好排查,可总得有个复现方法吧,正如我像他们吐槽的那样,总不可能我改一行代码,我输出个固件,然后发给远程的同事验证下吧,这样一来一回那效率得多低啊,搞不好真的通宵一晚都不一定搞得定。
所以,在我本地搭建可复现问题的环境至关重要,直接决定能不能快速找到问题的突破口。
冷静下来,我思考了一下,虽然客户那边的环境,域名还未完成备案不能做有效解析,但是从网络的基础知识我们可以知道,最终域名肯定是要转换成IP的呀,那我直接跳过域名,使用IP地址(这个IP地址自然是一个公网IP地址)连过去不就行了嘛?
虽然,远程同事告诉,这样行不通,但凭借我对网络通讯的理解,应该是可行的。
稍微补充一下:可能同事的意思是,不能走完网络通信的全部链路,因为TLS握手流程会涉及要域名的校验,但我只是想看下TLS握手阶段到底发生了啥错误,并不打算走完整个链路,所以可以一试。
其实要绕过域名也很简单,比如:
假设我们之前链接的MQTT broker地址是:gw.abc.com:12345 (12345是服务端口号)
然后客户部署的环境IP地址是:8.2.45.6
这时候,我们仅需要将MQTT broker的地址变成:8.2.45.6:12345 即可。
其他不需要改,我直接输出一个IP地址版本的固件做测试,果然在我本地复现了 -0x7a00 的现场问题。
同时,后面又不知怎么地不出现 -0x7a00错误了,转而报 -0x7780 错误码,同样还是TLS握手失败。
单从终端的log看,与现场发过来的一样,都是提示TLS握手失败,没有其他的有效信息了。
所以,必须得有其他手段来辅助排查了。
这此期间,根据远程同事的反馈,我也试过使用PC端的MQTT客户端,比如 MQTT.fx 做个简单测试,的确是可以完成TLS握手,并建立有效的网络通讯链路的。
同事,我还向运维的同事了解了下,尝试了解更多部署相关的信息,至于运维同事给我发出了多个 灵魂拷问 ,我来不及一一回答,我还得研究分析呢。
3.3 搭建网络抓包环境
根据过往做网络编程开发的经验,分析网络问题,从代码层面无法得到更多有效信息的时候,你应该要考虑去抓网络报文来做分析了。只有精准的网络报文可以告诉你第一现场的疑点在哪里。
为了控制本文的篇幅,如何搭建基于Wi-Fi通讯的终端抓包方法,我将会在另一篇博文里面介绍,感兴趣的可以自行跳转过去参考参考。
搭建好网络抓包环境之后,直接上wireshark,TLS的报文出来了。如下图所示:
图中仅展示了TLS握手部分的流程,不过问题也相对较清晰了,报文显示的流程如下:
终端发起 Client Hello ->
服务端回复 Server Hello + Certificate + Server Hello Done ->
终端响应 Client Key Exchange ->
服务器回复 Alert,错误描述是:Illegal Parameter ->
握手失败,终端报错。
关于TLS握手的详细流程下文会详讲,目前能拿到的有效信息就这么多了,剩下的就是如何从表面的一个个有效信息,层层突破,最终找到问题的根源。
从上面的抓包来看,至少这个 Illegal Parameter 是一个很关键的突破口。
4 深入分析
4.1 知识点补充
由于网络问题涉及的范围非常广,就拿本案例来说,就可能涉及到好几块的知识点,为了保证大家能读懂相关的内容,我特意将相关的知识点,简要地梳理一遍。如果认为自己对这几部分知识掌握得比较好的,可以跳到下一章节。
4.1.1 网络分层
这里不会详细将如何分层,仅给大家介绍一个宏观的概念,让你了解TCP和TLS所在的层次。
按照TCP/IP分层模型,从上到下,分别是:应用层、传输层、网络层、数据链路层、物理层。
参考了一些资料,找到这样一张图:
从这张图,我们可以比较直观地知道,TLS协议位于应用层(比如HTTP协议)和传输层(TCP协议)之间,那么从这么一个从属关系,我们可以大概知道什么时候该往下层找问题原因,什么时候该往上层找问题原因。
4.1.2 TCP层的三次握手和四次挥手
受篇幅原因,这里也不会详细阐述TCP的三次握手和四次挥手,感兴趣的可以参考其他资料自行了解。这个仅贴个图做简要说明:
从上一小节的中了解的TCP与TLS的层次关系,我们知道在建立TLS握手前,一定得先完成TCP的三次握手。
4.1.3 TLS的握手流程
TLS的握手流程是本案例的核心知识点,虽然博主也排查过不少TLS相关的问题,也参考学习过很多TLS相关的学习资料,对TLS的哥哥流程还是有所了解的。但无奈,我还是缺少自己从头到尾地用文字和图表梳理出属于自己的学习资料,所以为了能够比较好地展示相关的知识点给大家,我决定借用网友整理的博文来打辅助。全文大家可以去 这里 参考。
下面这张图,基本就高度概括了TLS握手的核心流程:
关于每一步出现的内容包括哪些要素,都可以从 这里 找到答案,自然最后我们分析问题,肯定是要结合这里的知识点进行突破。
4.1.4 数字证书相关知识
了解上面提及的TLS握手流程,就不得不提期间最关键的一步:证书校验,这个步骤是保证TLS安全绘画的核心所在。在了解证书校验之前,需要对以下几个知识点进行了解:
签名与验签
这里说的 签名 指的是 数字签名,而不是找某个大明星用笔签名,它是利用 非对称安全算法 实现数据安全的一种真实场景应用;而 验签 是 签名 的逆过程,正如早前我的一篇博文 【安全算法之概述】一文带你简要了解常见常用的安全算法 介绍 非对称安全算法那一章节介绍的那样;只有非对称安全算法,借助公私钥的功能才能发挥出最大功效。
数字证书
使用 签名和验签 等技术手段确保信任关系的一个数字凭证,可有效解决网络通讯中的安全信任问题。证书的内容一般包括:电子签证机关的信息、公钥用户信息、公钥、权威机构的签字和有效期等等。证书的格式和验证方法普遍遵循X.509 国际标准。
CA证书
CA是证书的签发机构,它是公钥基础设施(Public Key Infrastructure,PKI)的核心。CA是负责签发证书、认证证书、管理已颁发证书的机关。CA机构拥有一个证书,这个证书就是CA证书,一般我们也称之为 根证书 或 ROOT CA 。
证书链
在实践过程中,全球范围内,CA机构是有限的,为了能够更加高效地签发和管理数字证书,衍生出一个次级CA机构,它们的证书是顶级CA签发的,所以它的信任关系是由顶级CA来保证的。
同样的,还会有3级CA结构,4级CA机构,等等,他们的证书都是由上一级CA机构签发,从而形成一个链式的信任关系。而最终使用的证书,比如服务器端的证书,或者终端侧的证书,这种证书一般就不具备向下级签发证书的能力,他们属于整个证书信任链的最底端。
签名算法
根据 【安全算法之概述】一文带你简要了解常见常用的安全算法 的介绍,我们知道要执行一个签名操作,需要用到两种算法,一个用于加解密的 非对称算法,还有一种是用于计算信息摘要的 摘要算法。
一般来说,我们表示一种签名算法的写法是:xxxWithyyyEncryption,说明如下:
xxx:表示使用的摘要算法,
yyy:表示使用的非对称算法。
举个例子:SHA256WithRSAEncryption,说明如下:
该签名采用的摘要算法是 SHA256,非对称算法采用的是 RSA,至于RSA的密钥长度,这里是看不出来的。
4.2 深入分析
有了上面的这些知识点进行铺垫之后,我们尝试开始分析现场问题。
4.2.1 顺着表面错误往下查
回到抓到的现场报文,我们看到最终TLS握手的挂断是服务器发起的,而给出的错误描述是 illegal parameter 。
顺着这个关键词,我开始在网络上搜索相关的信息:
虽说帮助不是很大,但也不是一无所获,但至少知道这个错误代码的是 TLS握手中的 一大类错误,而且是跟 参数 有关的。
结合报错的前一条报文是客户端响应的 Client Key Exchange,自然是这个exchange里面包含了不合法的参数。
4.2.2 终端LOG不能丢
再回来终端侧的LOG,有个很明显的错误码 -0x7a00,那我们不妨查一下mbedtls中是如何定义这个错误码的:
include/mbedtls/ssl.h:90:#define MBEDTLS_ERR_SSL_BAD_HS_CERTIFICATE -0x7A00 /**< Processing of the Certificate handshake message failed. */
错误表明在处理 对方的证书 时遇到了错误。
稍微搜索了一下它的出现代码,嗯,还是有点多啊:
比如像这里的代码:
n = ( ssl->in_msg[i+1] << 8 ) | ssl->in_msg[i+2];
if( ssl->in_msg[i] != 0 ||
ssl->in_hslen != n + 3 + mbedtls_ssl_hs_hdr_len( ssl ) )
{
MBEDTLS_SSL_DEBUG_MSG( 1, ( "bad certificate message" ) );
mbedtls_ssl_send_alert_message( ssl, MBEDTLS_SSL_ALERT_LEVEL_FATAL,
MBEDTLS_SSL_ALERT_MSG_DECODE_ERROR );
return( MBEDTLS_ERR_SSL_BAD_HS_CERTIFICATE );
}
一时半会,肯定无法一下子判断是那个节点跳出去的。
另一方面,在复现的过程中,终端还出现过 -0x7780 的错误码,而这个错误码是可以对得上我抓的网络报文的,所以从它这里下手估计会好一些。
于是查了一下这个错误码:
#define MBEDTLS_ERR_SSL_UNEXPECTED_MESSAGE -0x7700 /**< An unexpected message was received from our peer. */
#define MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE -0x7780 /**< A fatal alert message was received from our peer. */
#define MBEDTLS_ERR_SSL_PEER_VERIFY_FAILED -0x7800 /**< Verification of our peer failed. */
结果给看懵了,这个 7780 不就是收到 ALERT 消息吗?嗯,的确是跟抓的网络报文对上了。
绕了一圈,又回来了,ALERT 的原因不就是 illegal parameter 吗?
illegal parameter 的原因是 Client Key Exchane 参数有误?
多留了一个心眼,还是去看看 Client Key Exchange 吧。
4.2.3 Client-Key-Exchange能有什么问题
通过前面的TLS握手流程知识,我们知道Client Key Exchange中的内容包括:
client_key_exchange: 客户端计算产生随机数字pre-master,并且用服务端公钥加密,发送给服务端,客户端以后具有自己的随机数A和服务端的随机数B,结合pre-master计算出协商得到的对称密钥.
所以这里面最终的就是客户端发送随机数的密文过去,如报文所示:
我保留了适当的猜想,既然对方说我的client-key-exchange有问题,这能有啥问题吗?这个密文就是使用对方的公钥算出来的,这个公钥来源于对方的证书,既然证书校验都已经过了,自然计算也不会有什么问题呀。
怀着试一试的态度,我找到了之前连我们其他公网环境能够顺利握手成功的报文,拿出来对比一下:
因为是密文,所以对比二进制数据肯定没啥意思,但是我注意到了他们的 长度 是不一样的。
出问题的是 386 字节,除去2个字节的类型编码,实际有效数据是 384 字节,而没有出问题的握手报文的有效数据长度是 256字节。
由于早前研究过RSA这种非对称算法,对它的公私钥运算还是铭记于心,一下子我想到了:
这段密文的长度是384字节,证明加密的 RSA公钥是3072位 的(384x8得到),因为这个RSA密钥长度的模长就是384;
而256字节的密文,对应的 RSA公钥是2048位 的(256x8得到),因为这个RSA密钥长度的模长正好就是256。
看到这里,我恍然大悟,卧槽,可能是mbedtls不支持RSA3072啊,所以算出来的密文是不正确的,导致对方解不开,于是给你报一个 illegal paramter 表示 你的数据有误,我解不开 ?
看样子好像有点顺利成章了,虽然还没真正找到根本原因,但总算是看到眉目了,有方向继续往下走了。
这个时候已经来到晚上快11点了,办公室没几个人了,还有老大跟我在追这个问题,没办法,远程的那帮同事在出着差呢,自然是不可能那么早下班的,还叮嘱我们有眉目的话,尽快即使同步过去。
由于实在没有更好的头绪,加上太晚了,脑子也不好使了,我就跟老大说,要不今天就先这样吧,明天过去我第一时间继续排查,争取解决。
其实我自己是知道的,明天必须解决,因为按照出差同事的安排,明天就是要完成端侧和移动侧的所有功能验证,再后台也就是周五还需要给客户做演示和相关技术培训。
倘若明天这个问题不能解决,那么出差的同事可能要顺延在那了,并且可能客户周末不一定会上班,那就要拖到下周了,这后果也不好。
还好,老大还是信任我,就答应我先回了,明天再接着排查,走之前把今天发现的问题点梳理下,给那边同步下。(这里的RSA384其实说的就是RSA3072bits算法,RSA384是以模长来命名的一种叫法)
回家的路上,还受到出差那边的负责人亲自语音过来同步确认,千叮万嘱明天一定得搞定,看了明天亚历山大。不管了,回去睡一觉再说。虽然是这么想,但回家的路上,还是时不时地会捋一捋当天的思路,以及留有的疑问点。
4.2.4 RSA算法出问题了吗
接着前一天的疑点,既然怀疑 client-key-exchange 中的密文因为使用RSA3072计算有误,从而推断mbedtls可能默认不支持RSA3072密钥长度,那么真的不支持吗?
于是,开始浏览并检索mbedtls的相关代码,先是从mbedtls的配置文件 mbedtls_config.h 中找到了 MBEDTLS_RSA_C,这个选项表示当前mbedtls支持RSA算法,但并没有说支持的最大密钥位数是多少,还得接着找线索。
在找的过程中,发现mbedtls中实现RSA算法使用的是 BIGNUM ,于是首先找到它的头文件 bignum.h ,快速浏览,我找到了一下相关的一些宏定义:
/*
- Maximum size of MPIs allowed in bits and bytes for user-MPIs.
- ( Default: 512 bytes => 4096 bits, Maximum tested: 2048 bytes => 16384 bits )
- Note: Calculations can temporarily result in larger MPIs. So the number
- of limbs required (MBEDTLS_MPI_MAX_LIMBS) is higher.
*/
#define MBEDTLS_MPI_MAX_SIZE 1024 /**< Maximum number of bytes for usable MPIs. */
#endif /* !MBEDTLS_MPI_MAX_SIZE */
#define MBEDTLS_MPI_MAX_BITS ( 8 * MBEDTLS_MPI_MAX_SIZE ) /**< Maximum number of bits for usable MPIs. */
结合注释,大致了解了,这个 MBEDTLS_MPI_MAX_SIZE 就是能支持的最大RSA模长,而 MBEDTLS_MPI_MAX_BITS 对应的就是RSA密钥位数的最大值,这两个值有一个8倍的数量关系。
回到我们的疑点,mbedtls的默认配置是否支持RSA3072bits,从上面就知道了答案:它最大支持是8192bits,自然它就支持3072bits。
线索又断了,但是我们现在可以往另一方面想了,这段密文使用RSA加密暂且可以认为是没有问题的,那这就证明它使用的RSA公钥的密钥长度是3072位。
4.2.5 对方证书看似有很大问题
顺着上面的思路,既然计算密文的RSA公钥长度是 3072 位,那就证明在当前握手流程中,TLS认为对方证书中的公钥就是RSA3072位的。
但,实际上是不是 RSA3072 位呢?我们需要一些手段来确认下。
还是从前面抓到的网络报文入手,我们找到对应证书部分的内容;根据TLS握手流程的知识我们知道,在握手阶段,证书都是明文的字节流传输的,所以在wireshark中可以直接解析查看。
于是我们从wireshark中看到了这样的一些内容:
由于网络分包的原因,一般我们在wireshark里面会看到 Server-Hello + Certificate + Server-Hello-Done 在一个报文条里显示,但实际它是多条报文合并而来的。
由此我们可知,在TLS握手流程中,服务器端下发了3级证书,从上到下分别是:服务器证书 -》次级CA证书 -》顶级CA证书 。
而根据前面提及的证书链的相关知识,我们知道签发的从属关系,应该是:顶级CA 签发 次级CA,次级CA 再签发服务器证书。
如何使用wireshark分析一个数字证书?
其实wireshark已经帮你把数字证书的每个字段都解析好了,你只需要顺着那些节点,一个个点开查看即可。
以顶级CA证书为例,我们看下它的内容,我们重点关注一下几个内容:
signature (sha1WithRSAEncryption):表明证书签名时使用的算法是 SHA1withRSA。
这里的
issuer: rdnSequence (0):表明证书 签发者 信息,这里的信息包括CN(commonName)、ON(organizationName)、LN(localityName)等等。
这些缩写都有特定的含义,需要查阅X509规范中对这些字段的描述,一般来说,我们从CN字段就可以简单了解这个 角色 的身份了。
subject: rdnSequence (0):表明证书 持有者 的信息,格式与issuer: rdnSequence (0)一样,都是CN、ON、LN这些信息;
由于我们分析是顶级CA,它位于信任链的最顶端,所以它的证书比较特别,它是自签发的,所以issuer字段和subject字段都是一模一样的;如果不是顶级CA证书的话,这两者信息恰好能体现签发的从属关系。
validity:表明证书的 有效期,比如这个CA证书的有效期就是:24年,到2038年过期。
subjectPublicKeyInfo:这部分就是证书中包含的【证书持有者的】公钥部分。
以RSA算法为例,一个RSA公钥主要包含两部分:公钥模长、公钥指数(常用值 65537,即0x00010001)
公钥的模长直接决定了RSA的密钥长度,位数越高,加解密的复杂度越高,同意需要的运算能力以及RAM也会更高。
RSA的公钥指数理论上可以随意选择的,但常用的典型值是65537。
encrypted:这部分是证书的签名字段,它是由 签发者 生成的(具体的做法是使用签发者的私钥对证书的哈希做加密算出来),签名字段是用于别人做证书验签使用的。
Extension (id-ce-basicConstraints:critical: True:这个字段主要是表明该证书是否具备签发下级证书的能力,比如这个顶级CA证书,自然是具备这个能力的。
没有wireshark怎么分析一个数字证书?
这里补充一点,如果我们没有wireshark工具做辅助,比如在实践项目中,别人可能以文件的形式发了一个数字证书文件给你,你怎么去查看并分析这个证书文件呢?
当然,工具方法有很多中,这里我推荐使用 openssl 的命令行工具。
一般来说数字证书都是采用国际通用的X509,这种格式的数字证书,每个字段代表啥含义都是约定好的,大家按照规范解析就好了。
另一方面,数字证书文件一般有两种表面格式,可以从后缀名简单做个判断:
后缀名为(.crt .pem .cer):一般就是 PEM 格式,这种格式是明文的文件,文本工具打开后可以看到一大段用base64加密过的明文字符,并在文件的开头和结尾有 ——-BEGIN CERTIFICATE——- 和 ——-END CERTIFICATE——- 字样。
后缀名为(.der):一般就是 DER 格式,这种格式其实是个二进制形式,所以文本工具打开是会乱码的,需要用二进制工具来查看。
使用openssl工具可以实现两个格式的转换,可以网上找一找教程,一般工程实践中,使用 PEM 格式居多。
这里介绍一下,使用openssl命令行查看证书内容,以下操作假设你的编译环境以及具备了openssl命令行环境,一般的Linux发行版本都自带了openssl命令行,Windows环境的话,客户需要装opensll,git-bash工具,以下以Windows环境git-bash中操作进行介绍:
输入这个命令查看openssl的版本,如果提示命令不存在,则标识当前环境不支持openssl命令行
$ openssl version
OpenSSL 1.1.1i 8 Dec 2020
输入这个命令解析X509证书,假设当前目录已经有一个名叫ca_root.pem的顶级CA证书;其中 inform PEM 可省略,默认解析的就是PEM格式。
$ openssl x509 -in ca_root.pem -text -inform PEM
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 1 (0x1)
Signature Algorithm: sha1WithRSAEncryption
Issuer: C = GB, ST = Greater Manchester, L = Salford, O = Comodo CA Limited, CN = AAA Certificate Services
Validity
Not Before: Jan 1 00:00:00 2004 GMT
Not After : Dec 31 23:59:59 2028 GMT
Subject: C = GB, ST = Greater Manchester, L = Salford, O = Comodo CA Limited, CN = AAA Certificate Services
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public-Key: (2048 bit)
Modulus:
00:be:40:9d:f4:6e:e1:ea:76:87:1c:4d:45:44:8e:
be:46:c8:83:06:9d:c1:2a:fe:18:1f:8e:e4:02:fa:
f3:ab:5d:50:8a:16:31:0b:9a:06:d0:c5:70:22:cd:
49:2d:54:63:cc:b6:6e:68:46:0b:53:ea:cb:4c:24:
c0:bc:72:4e:ea:f1:15:ae:f4:54:9a:12:0a:c3:7a:
b2:33:60:e2:da:89:55:f3:22:58:f3:de:dc:cf:ef:
83:86:a2:8c:94:4f:9f:68:f2:98:90:46:84:27:c7:
76:bf:e3:cc:35:2c:8b:5e:07:64:65:82:c0:48:b0:
a8:91:f9:61:9f:76:20:50:a8:91:c7:66:b5:eb:78:
62:03:56:f0:8a:1a:13:ea:31:a3:1e:a0:99:fd:38:
f6:f6:27:32:58:6f:07:f5:6b:b8:fb:14:2b:af:b7:
aa:cc:d6:63:5f:73:8c:da:05:99:a8:38:a8:cb:17:
78:36:51:ac:e9:9e:f4:78:3a:8d:cf:0f:d9:42:e2:
98:0c:ab:2f:9f:0e:01:de:ef:9f:99:49:f1:2d:df:
ac:74:4d:1b:98:b5:47:c5:e5:29:d1:f9:90:18:c7:
62:9c:be:83:c7:26:7b:3e:8a:25:c7:c0:dd:9d:e6:
35:68:10:20:9d:8f:d8:de:d2:c3:84:9c:0d:5e:e8:
2f:c9
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Subject Key Identifier:
A0:11:0A:23:3E:96:F1:07:EC:E2:AF:29:EF:82:A5:7F:D0:30:A4:B4
X509v3 Key Usage: critical
Certificate Sign, CRL Sign
X509v3 Basic Constraints: critical
CA:TRUE
X509v3 CRL Distribution Points:
Full Name:
URI:http://crl.comodoca.com/AAACertificateServices.crl
Full Name:
URI:http://crl.comodo.net/AAACertificateServices.crl
Signature Algorithm: sha1WithRSAEncryption
08:56:fc:02:f0:9b:e8:ff:a4:fa:d6:7b:c6:44:80:ce:4f:c4:
c5:f6:00:58:cc:a6:b6:bc:14:49:68:04:76:e8:e6:ee:5d:ec:
02:0f:60:d6:8d:50:18:4f:26:4e:01:e3:e6:b0:a5:ee:bf:bc:
74:54:41:bf:fd:fc:12:b8:c7:4f:5a:f4:89:60:05:7f:60:b7:
05:4a:f3:f6:f1:c2:bf:c4:b9:74:86:b6:2d:7d:6b:cc:d2:f3:
46:dd:2f:c6:e0:6a:c3:c3:34:03:2c:7d:96:dd:5a:c2:0e:a7:
0a:99:c1:05:8b:ab:0c:2f:f3:5c:3a:cf:6c:37:55:09:87:de:
53:40:6c:58:ef:fc:b6:ab:65:6e:04:f6:1b:dc:3c:e0:5a:15:
c6:9e:d9:f1:59:48:30:21:65:03:6c:ec:e9:21:73:ec:9b:03:
a1:e0:37:ad:a0:15:18:8f:fa:ba:02:ce:a7:2c:a9:10:13:2c:
d4:e5:08:26:ab:22:97:60:f8:90:5e:74:d4:a2:9a:53:bd:f2:
a9:68:e0:a2:6e:c2:d7:6c:b1:a3:0f:9e:bf:eb:68:e7:56:f2:
ae:f2:e3:2b:38:3a:09:81:b5:6b:85:d7:be:2d:ed:3f:1a:b7:
b2:63:e2:f5:62:2c:82:d4:6a:00:41:50:f1:39:83:9f:95:e9:
36:96:98:6e
从输出的结果看,我们照样取到了wireshark解析出来的那些数据,也用一些比较友好的递进关系展现给我们,非常地方便查看。
网络报文中的证书链信息
用上面的方法,我们分析下本次TLS握手中的各级证书,筛选出其主要参数如表所示:
从这3个证书可以发现:证书1是被证书2签发,而证书2被证书3签发,证书3是顶级CA,它是自签发的。
上面这个表格中,最重要的部分是 证书签名算法 和 证书公钥长度:
根据上面的关于 签名算法 的小知识,我们可以知道一个很重要信息,这个信息直接就差不多可以解开谜团了。
服务器证书采用的是 sha384WithRSAEncryption 签名的,而它本身的RSA密钥长度是 2048bits;次级CA证书的签名算法是 sha256WithRSAEncryption ,而它本身的RSA密钥长度是 3072bits。
这说明啥?
联想起,我们上面分析的 Client-Key-Exchange,传递的密文不是 384字节吗?对应的RSA密钥长度正是 3072bits。
所以,我们是不是有理由推荐,终端在计算 Client-Key-Exchange 的时候,采用了 次级CA证书的公钥,而不是服务器证书的公钥?
按理说,mbedtls是一个很成熟的TLS实现库,理论上不可能犯这么低级错误的。
那么,原因只有一个,很有可能,由于某些配置导致,TLS握手(证书验签等环节)出问题了,从而终端 把次级CA证书当作了服务器证书使用。
至于,为什么会错误这个 错误,看来只能分析 mbedtls 的实现源码了。
4.2.6 mbedtls中证书校验的实现有问题吗
以前我们知道openssl实现的TLS,但由于它过于庞大,一般资源紧张的嵌入式系统都倾向于选择 mbedtls。
相对而言,轻量了许多,但是虽说轻量,但TLS本身流程就比较复杂,各种流程跳转,各种算法实现,如果找不到方向的话,去看源码绝对是一头雾水。
我的建议是:你要想去看mbedtls的实现源码,至少你应该把TLS的相关流程要了然于胸,然后有针对性地看每个流程(状态)的实现!
以我手上的版本为例:
mbed TLS ChangeLog (Sorted per branch, date)
= mbed TLS 2.16.0 branch released 2018-12-21
基本可以按以下方式进行梳理下:
因为我要排查TLS证书校验部分,所以我会重点排查这几个文件:
因为,TLS流程中 CERTIFICATE 跟着 SERVER_HELLO 之后,所以我会在 ssl_cli.c (实现TLS客户端的源码)文件里面先检索 server_hello,这样就很容易找到这样一个函数:
static int ssl_parse_server_hello( mbedtls_ssl_context *ssl )
顺着这个函数,我们找到它的调用之处,一下子就发现新大陆了:
/*
- SSL handshake -- client side -- single step
*/
int mbedtls_ssl_handshake_client_step( mbedtls_ssl_context *ssl )
{
int ret = 0;
if( ssl->state == MBEDTLS_SSL_HANDSHAKE_OVER || ssl->handshake == NULL )
return( MBEDTLS_ERR_SSL_BAD_INPUT_DATA );
MBEDTLS_SSL_DEBUG_MSG( 2, ( "client state: %d", ssl->state ) );
switch( ssl->state )
{
case MBEDTLS_SSL_HELLO_REQUEST:
ssl->state = MBEDTLS_SSL_CLIENT_HELLO;
break;
case MBEDTLS_SSL_CLIENT_HELLO:
ret = ssl_write_client_hello( ssl );
break;
case MBEDTLS_SSL_SERVER_HELLO:
ret = ssl_parse_server_hello( ssl );
break;
case MBEDTLS_SSL_SERVER_CERTIFICATE:
ret = mbedtls_ssl_parse_certificate( ssl );
break;
case MBEDTLS_SSL_SERVER_KEY_EXCHANGE:
ret = ssl_parse_server_key_exchange( ssl );
break;
case MBEDTLS_SSL_CERTIFICATE_REQUEST:
ret = ssl_parse_certificate_request( ssl );
break;
case MBEDTLS_SSL_SERVER_HELLO_DONE:
ret = ssl_parse_server_hello_done( ssl );
break;
case MBEDTLS_SSL_CLIENT_CERTIFICATE:
ret = mbedtls_ssl_write_certificate( ssl );
break;
case MBEDTLS_SSL_CLIENT_KEY_EXCHANGE:
ret = ssl_write_client_key_exchange( ssl );
break;
case MBEDTLS_SSL_CERTIFICATE_VERIFY:
ret = ssl_write_certificate_verify( ssl );
break;
case MBEDTLS_SSL_CLIENT_CHANGE_CIPHER_SPEC:
ret = mbedtls_ssl_write_change_cipher_spec( ssl );
break;
case MBEDTLS_SSL_CLIENT_FINISHED:
ret = mbedtls_ssl_write_finished( ssl );
break;
看明白了吗?这里就是一个 有限状态机 的处理机制,TLS握手流程中的每一步对应一个状态,分阶段去处理。
这样就很清晰了,比如我要排查 证书校验 相关的,我只需要关注 MBEDTLS_SSL_SERVER_CERTIFICATE 即可,后面我要排查 Client-Key-Exchange,我只需要关注 MBEDTLS_SSL_CLIENT_KEY_EXCHANGE 即可。
先看下 证书校验 部分,这里因篇幅原因,我就只梳理关键的节点调用,辅以适当的注释:
mbedtls_ssl_handshake_client_step ->
mbedtls_ssl_parse_certificate ->
ssl_parse_certificate_chain ->
mbedtls_x509_crt_verify_restartable ->
x509_crt_verify_chain ->
x509_crt_find_parent ->
所以这里很关键的代码就是在 证书链 校验上面,我把关键的代码贴出来:
/*
- Build and verify a certificate chain
- Given a peer-provided list of certificates EE, C1, ..., Cn and
- a list of trusted certs R1, ... Rp, try to build and verify a chain
-
EE, Ci1, ... Ciq [, Rj]
- such that every cert in the chain is a child of the next one,
- jumping to a trusted root as early as possible.
- Verify that chain and return it with flags for all issues found.
- Special cases:
-
- EE == Rj -> return a one-element list containing it
-
- EE, Ci1, ..., Ciq cannot be continued with a trusted root
- -> return that chain with NOT_TRUSTED set on Ciq
- Tests for (aspects of) this function should include at least:
-
-
-
- EE -> intermedate CA -> trusted root
-
- if relevant: EE untrusted
-
- if relevant: EE -> intermediate, untrusted
- with the aspect under test checked at each relevant level (EE, int, root).
- For some aspects longer chains are required, but usually length 2 is
- enough (but length 1 is not in general).
- Arguments:
-
- [in] crt: the cert list EE, C1, ..., Cn
-
- [in] trust_ca: the trusted list R1, ..., Rp
-
- [in] ca_crl, profile: as in verify_with_profile()
-
- [out] ver_chain: the built and verified chain
-
Only valid when return value is 0, may contain garbage otherwise!
-
Restart note: need not be the same when calling again to resume.
-
- [in-out] rs_ctx: context for restarting operations
- Return value:
-
- non-zero if the chain could not be fully built and examined
-
- 0 is the chain was successfully built and examined,
-
even if it was found to be invalid
*/
static int x509_crt_verify_chain(
mbedtls_x509_crt *crt,
mbedtls_x509_crt *trust_ca,
mbedtls_x509_crl *ca_crl,
const mbedtls_x509_crt_profile *profile,
mbedtls_x509_crt_verify_chain *ver_chain,
mbedtls_x509_crt_restart_ctx *rs_ctx )
{
int ret;
uint32_t *flags;
mbedtls_x509_crt_verify_chain_item *cur;
mbedtls_x509_crt *child;
mbedtls_x509_crt *parent;
int parent_is_trusted;
int child_is_trusted;
int signature_is_good;
unsigned self_cnt;
child = crt;
self_cnt = 0;
parent_is_trusted = 0;
child_is_trusted = 0;
while( 1 ) {
cur = &ver_chain->items[ver_chain->len];
cur->crt = child;
cur->flags = 0;
ver_chain->len++;
flags = &cur->flags;
if( mbedtls_x509_time_is_past( &child->valid_to ) )
*flags |= MBEDTLS_X509_BADCERT_EXPIRED;
if( mbedtls_x509_time_is_future( &child->valid_from ) )
*flags |= MBEDTLS_X509_BADCERT_FUTURE;
if( child_is_trusted )
return( 0 );
if( x509_profile_check_md_alg( profile, child->sig_md ) != 0 )
*flags |= MBEDTLS_X509_BADCERT_BAD_MD;
if( x509_profile_check_pk_alg( profile, child->sig_pk ) != 0 )
*flags |= MBEDTLS_X509_BADCERT_BAD_PK;
if( ver_chain->len == 1 &&
x509_crt_check_ee_locally_trusted( child, trust_ca ) == 0 )
{
return( 0 );
}
#if defined(MBEDTLS_ECDSA_C) && defined(MBEDTLS_ECP_RESTARTABLE)
find_parent:
#endif
/* Look for a parent in trusted CAs or up the chain */
ret = x509_crt_find_parent( child, trust_ca, &parent,
&parent_is_trusted, &signature_is_good,
ver_chain->len - 1, self_cnt, rs_ctx );
/* No parent? We're done here */
if( parent == NULL )
{
*flags |= MBEDTLS_X509_BADCERT_NOT_TRUSTED;
return( 0 );
}
/* Count intermediate self-issued (not necessarily self-signed) certs.
* These can occur with some strategies for key rollover, see [SIRO],
* and should be excluded from max_pathlen checks. */
if( ver_chain->len != 1 &&
x509_name_cmp( &child->issuer, &child->subject ) == 0 )
{
self_cnt++;
}
/* path_cnt is 0 for the first intermediate CA,
* and if parent is trusted it's not an intermediate CA */
if( ! parent_is_trusted &&
ver_chain->len > MBEDTLS_X509_MAX_INTERMEDIATE_CA )
{
/* return immediately to avoid overflow the chain array */
return( MBEDTLS_ERR_X509_FATAL_ERROR );
}
/* signature was checked while searching parent */
if( ! signature_is_good )
*flags |= MBEDTLS_X509_BADCERT_NOT_TRUSTED;
/* check size of signing key */
if( x509_profile_check_key( profile, &parent->pk ) != 0 )
*flags |= MBEDTLS_X509_BADCERT_BAD_KEY;
/* Check trusted CA's CRL for the given crt */
*flags |= x509_crt_verifycrl( child, parent, ca_crl, profile );
/* prepare for next iteration */
child = parent;
parent = NULL;
child_is_trusted = parent_is_trusted;
signature_is_good = 0;
}
}
核心流程就是,找到证书的签发者,然后验签,并校验证书的各个要素。
最后完成校验后,整个流程会认为存在 ssl->session_negotiate->peer_cert 证书链中的第一个证书就是服务器证书,然后下一阶段执行 Client-Key-Exchange 的时候,就会取这个证书的公钥。所以我后面还在这里补了一个 注释 。
MBEDTLS_SSL_DEBUG_CRT( 3, "peer certificate", ssl->session_negotiate->peer_cert );
再接着看下 Client-Key-Exchange 的流程:
static int ssl_write_client_key_exchange( mbedtls_ssl_context *ssl )
{
#if defined(MBEDTLS_KEY_EXCHANGE_RSA_ENABLED)
if( ciphersuite_info->key_exchange == MBEDTLS_KEY_EXCHANGE_RSA )
{
i = 4;
if( ( ret = ssl_write_encrypted_pms( ssl, i, &n, 0 ) ) != 0 )
return( ret );
}
else
#endif /* MBEDTLS_KEY_EXCHANGE_RSA_ENABLED */
}
这里会还会涉及在 Server-Hello 时,对方选择的密钥套件有关,这个密钥套件决定了在 密钥交换(Key-Exchange) 阶段使用的算法,比如我抓到的报文是 Cipher Suite: TLS_RSA_WITH_AES_256_CBC_SHA256 (0x003d)。
所以使用的 MBEDTLS_KEY_EXCHANGE_RSA 。
接下来是数据的加密:
- Generate a pre-master secret and encrypt it with the server's RSA key
*/
static int ssl_write_encrypted_pms( mbedtls_ssl_context *ssl,
size_t offset, size_t *olen,
size_t pms_offset )
{
if( ssl->session_negotiate->peer_cert == NULL )
{
MBEDTLS_SSL_DEBUG_MSG( 2, ( "certificate required" ) );
return( MBEDTLS_ERR_SSL_UNEXPECTED_MESSAGE );
}
if( ! mbedtls_pk_can_do( &ssl->session_negotiate->peer_cert->pk,
MBEDTLS_PK_RSA ) )
{
MBEDTLS_SSL_DEBUG_MSG( 1, ( "certificate key type mismatch" ) );
return( MBEDTLS_ERR_SSL_PK_TYPE_MISMATCH );
}
if( ( ret = mbedtls_pk_encrypt( &ssl->session_negotiate->peer_cert->pk,
p, ssl->handshake->pmslen,
ssl->out_msg + offset + len_bytes, olen,
MBEDTLS_SSL_OUT_CONTENT_LEN - offset - len_bytes,
ssl->conf->f_rng, ssl->conf->p_rng ) ) != 0 )
{
MBEDTLS_SSL_DEBUG_RET( 1, "mbedtls_rsa_pkcs1_encrypt", ret );
return( ret );
}
}
mbedtls_pk_encrypt( &ssl->session_negotiate->peer_cert->pk 这就是应证了之前的假设,果然它就是使用 次级CA证书 的公钥计算的,自然下一步服务器收到 Client-Key-Exchange 就直接拒绝了。
4.2.7 到底哪个环节出了问题
看了上面的源码分析,似乎mbedtls的证书校验,并没有问题。但是分析完上面,结合之前对那个各个证书内容分析,我已经能够大胆推测了,可能是SHA384算法不支持。
最早我的怀疑是:RSA3072bits 不支持,但已经被推翻了,由于次级CA证书的RSA是3072bits,所以它选用 SHA384WithRSAEncryption 做签名算法,也算情理之中,即便主流的SHA算法是 SHA256.
顺着SHA算法的支持情况,一查果然发现问题所在,原来mbedtls的配置里,默认就只开了 SHA1和SHA256,且预留了配置项,开启其他算法,具体下一小节再细讲。
所以理论上,只要我把 SHA384 算法的支持打开,一切就会顺利成章地完成TLS握手。
但是,如果说 SHA384 算法不支持,那就不应走到 Client-Key-Exchange 阶段啊,在证书校验环节就应该要卡掉!
讲到这里,回想下,可能是之前对证书校验理解不够深入,在调试源码的时候,认为有些情况可以跳过,所以这种算法不支持的场景就被我们忽略了,直接导致的后果就是错误被蔓延了,直到 Client-Key-Exchange 阶段才出现了。
4.2.8 mbedtls的正确使用姿势
各类算法的支持
mbedtls支持非常多算法,基本主流的算法都支持,包括上面提及的SHA算法,还有各种对称算法、非对称算法。
在配置上,预留了很人性化的配置菜单来使能各种算法:
下次再遇到类似的问题,多一个排查方向了:相关算法的支持是否打开了 ?
TLS相关的LOG调试
上面搞了这么多源码分析,唯一还未提及配合LOG调试,其实我主要是先想弱化LOG的分析作用,先让大家从原理流程上把握整个分析过程。
mbedtls再LOG上还是比较人性化的,预留了应用层 个性化 打印LOG的需求,它采用的注册回调的机制来实现的。
它的使用方法如下:
再TLS连接初始化时,我们会调用一些列的 配置初始化接口,其中有这么一个接口:
/**
*/
void mbedtls_ssl_conf_dbg( mbedtls_ssl_config *conf,
void (*f_dbg)(void *, int, const char *, int, const char *),
void *p_dbg );
它可以将 LOG 打印的接口回调到应用层自己的代码中,比如像我这,我就会这样写:
static void my_tls_debug(void *param, int level, const char *file, int line,
const char *str)
{
((void)level); //忽略LOG等级
printf("[%s:%d] %s
", file, line, str); //同时打印文件名和行号
}
mbedtls_ssl_conf_dbg(conf, my_tls_debug, NULL)
这个还是挺有用的,可以你自己来控制LOG等级,LOG还是比较详细的。
同时,它还有一个专门设置LOG等级的接口,长这样:
#if defined(MBEDTLS_DEBUG_C)
mbedtls_debug_set_threshold((int)DEBUG_LEVEL)
#endif
-
-
- 0 No debug
-
- 1 Error
-
- 2 State change
-
- 3 Informational
-
- 4 Verbose
应用层完全可以自己控制LOG的内容和格式。
怎么样,调试起来,肯定会事半功倍吧?
如果把LOG打开,本案例的问题会怎么样呢?
留给感兴趣的读者自己尝试一下吧。。。
5 修复验证
修复验证往往是最简单的一环,因为前面已经有了大量的理论分析和实践猜想。
5.1 问题修复
找到问题的突破口,修复起来自然是很简单,仅仅需要把 SHA384 算法支持上就可以了。
在我们已实现的代码框架下,仅仅需要把 MBEDTLS_CONFIG_CRYPTO_SHA512 这个配置项使能上就可以了,甚至一行代码都不需要改。
至于为啥又跟 SHA512 扯上关系,建议了解一下SHA384和SHA512的源码实现,他们本自同根生,一条路子出来的,只不过长度不一样而已。
5.2 问题验证
把配置项选上,重新输出一个固件,自己验证一把,可以完成TLS握手,随后把输出的固件同步远程出差的同事,不一会也得到了正确的响应,群里一致收到了好评,而此时的时间大概在上午的10点半。
实践证明,还是上午的排查思路更清晰流畅,单靠晚上的加班加点不见得能死磕问题!
回头想想也是慌,要不今天搞不定,这可就不是这个声音了,同时团队的公信力也会大打折扣了。
6 经验总结
越是紧急的问题,越是需要冷静地分析:实践证明,着急解决不了任何问题,反而会让自己陷入一个排查盲区,无法自拔;
非对称RSA运算(加密解密)的数据长度、密钥的长度、模长的关系,是一个非常重要的突破口;
安全算法非常多,基本上掌握最核心的几种就可以应付绝大多数的应用场景;对算法的敏感性,也决定了对TLS这类问题的排查速度;
TLS中的证书校验,往往是实现TLS安全的最关键的一环,也往往是最容易出问题的一环;
排查网络问题的基本思路:先看外网能不能通,比如ping外网看下;其次看下TCP服务器能不能通,直接使用IP+端口的形式访问;最后再看看TLS握手流程,有机会一定要抓包分析;10个TLS问题9.9个抓包可看出问题的表面原因;
对比发现问题的能力,在嵌入式开发中依然显得非常重要;有对比,再适当迁移,往往能打开更多的突破口;
排查问题亘古不变的原则:大胆假设,小心求证;
原作者:recan