如何建立TLS连接?TLS握手失败可能这个原因!

2023年 7月 24日 124.9k 0

签前面三个案例里的HTTP都没加密,使排查工作省去不少麻烦,抓包文件里直接就看清应用层信息。

但现实越来越多站点做HTTPS加密,所以像前面的三讲那样Wireshark里直接看到应用层信息的 case 越来越少。

根据w3techs.com 调查数据,Internet 78%以上的站点默认HTTPS。要对Internet上的问题做应用层方面的分析,TLS是绕不开的坎。

我主要内网问题,不关心太多HTTPS?勉强算对,但随各大企业不断推进零信任( Zero Trust)安全策略,越来越多内网流量也终将运行在HTTPS,内网和公网将无差。

所以,掌握HTTPS/TLS的相关知识和排查技巧对网络排查必备。

1 啥是HTTPS?

首先我们要认识一下HTTPS。它其实不是某个独立的协议,而是HTTP over TLS,也就是把HTTP消息用TLS进行加密传输。两者相互协同又各自独立,依然遵循了网络分层模型的思想:

加密技术是HTTPS的核心。

加密技术基础

加密技术其实也是一个古老的话题。早在公元前400年,斯巴达人就创造了密码棒加密法:纸条缠绕在一根木棒上,然后在纸上写字,这张纸条离开这根木棒后,就无法正确读取了。要“破解”它,就得找到同样粗细的木棒,然后把纸条绕上去后,才能解读。

纸条相当于密文,木棒相当于密钥。因为加密和解密用的木棒是相同的,所以它属于对称加密算法。

2 TLS基础

TLS同时使用对称算法、非对称算法。

TLS整个过程分为:

  • 握手阶段,完成验证,协商出密码套件,进而生成对称密钥,用于后续的加密通信
  • 加密通信阶段,数据由对称加密算法来加解密

TLS综合利用对称算法、非对称算法的优点:

  • 对称算法效率高

  • 非对称算法安全性高

二者结合兼顾效率和安全性!

TLS问题排查也就面临两类问题:

  • TLS握手阶段

    真正加密还没开始,所以依托明文形式的握手信息,还可能找到握手失败原因。该阶段要掌握TLS握手原理和技术细节,才能指导展开排查工作

  • TLS通信过程

    加密已开始,所有数据已是密文。假如应用层发生啥,而我们又看不到,如何排查?要 把密文解密,才能找到根因。

“TLS要是能随便解密,是不是说明这协议还有漏洞?”TLS很安全的。这里说的解密肯定有前提条件,和数据安全性不冲突。

案例学习TLS握手失败的问题排查思路。

3 案例:TLS握手失败

3.1 问题原因

如域名不匹配、证书过期等。这些问题一般都可通过“忽略验证”这简单操作来跳过。如在浏览器的警告弹窗里点击“忽略”,就能让整个TLS过程继续。

还有一些问题无法跳过。

有个应用要访问k8s集群的API server。因为我们有很多集群,相应API server也有很多。从同一台客户端:

  • 访问API server 1可以
  • 但访问API server 2不行

发现失败原因就是TLS握手失败:

在客户端的应用日志里的错误:

javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure

只说握手失败了。网络排查两大鸿沟之一的 应用现象跟网络现象之间的鸿沟:看得懂应用层日志,但不知道网络到底发生啥。

这里日志也无法告诉我们:到底TLS握手哪里问题。所以要做点别的事。

3.2 排除服务端问题

先用趁手小工具 curl,从这台客户端发起对API server 2(握手失败的)的TLS握手,发现能成功。这说明,API server 2至少某些条件下正常工作。

当时输出:

curl -vk https://api.server.777.abcd.io
* Rebuilt URL to: https://api.server.777.abcd.io/
* Trying 10.100.20.200...
* Connected to api.server.777.abcd.io (10.100.20.200) port 443 (#0)
* found 153 certificates in /etc/ssl/certs/ca-certificates.crt
* found 617 certificates in /etc/ssl/certs
* ALPN, offering http/1.1
* SSL connection using TLS1.2 / ECDHE_RSA_AES_128_GCM_SHA256
* server certificate verification SKIPPED
* server certificate status verification SKIPPED
* common name: server (does not match 'api.server.777.abcd.io')
* server certificate expiration date OK
* server certificate activation date OK
* certificate public key: RSA
* certificate version: #3
* subject: CN=server
* start date: Thu, 24 Sep 2020 21:42:00 GMT
* expire date: Tue, 23 Sep 2025 21:42:00 GMT
* issuer: C=US,ST=San Francisco,L=CA,O=My Company Name,OU=Org Unit 2,CN=kubernetes-certs
* compression: NULL

第8行即协商出的密码套件 * SSL connection using TLS1.2 / ECDHE_RSA_AES_128_GCM_SHA256

既然curl能TLS握手成功,是不是客户端程序本身问题?开始“问题复现”。前一篇文章讨论偶发性问题的“复现+抓包”策略,而这问题必现,只要发起一次请求,做好抓包。

看抓包文件:

“话不投机半句多”,客户端就发个Client Hello报文,服务端就回复TLS Alert报文,结束这次对话。

为啥聊不起来?看Alert报文:

编号40,指代Handshake Failure错误类型。要了解这错误类型的具体定义。

正确做法

去RFC寻找答案*,而不是随意去网络搜索,因为可能被一些信息误导。

因为这次握手用TLS1.2协议,看 RFC5246。在这个RFC里,找到Alert Protocol:

   handshake_failure
      Reception of a handshake_failure alert message indicates that the
      sender was unable to negotiate an acceptable set of security
      parameters given the options available.  This is a fatal error.

结合实际场景,段意:“基于已收到的Client Hello报文中的选项,TLS服务端无法协商出一个可接受的安全参数集”。安全参数集在这指加密算法套件 Cipher Suite。

3.3 Cipher Suite

TLS中真正的数据传输用的加密方式是 对称加密;对称密钥的交换使用 非对称加密。

TLS握手阶段要在下面四环里实现不同类型的安全性,TLS“四大护法”:

  • 密钥交换算法:保证对称密钥的交换是安全,典型算法DHE、ECDHE
  • 身份验证和签名算法:确认服务端的身份,即对证书验证,非对称算法就用在这。典型算法RSA、ECDSA

补充:如双向验证(mTLS),服务端会验证客户端的证书。

  • 对称加密算法:对应用层数据加密,典型算法AES、DES
  • 消息完整性校验算法:确保消息不被篡改,典型算法SHA1、SHA256

每个类型都有不同具体算法实现,它们的组合就是Cipher Suite。

典型的密码套件

TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA(0xc013)
  • TLSTLS协议
  • ECDHE,密钥交换算法,双方通过它就不用直接传输对称密钥,只需通过交换双方生成的随机数等信息,就可以各自计算出对称密钥
  • RSA,身份验证和签名算法,主要是客户端来验证服务端证书的有效性,确保服务端是本尊
  • AES128_CBC,对称加密算法,应用层的数据就用它加解密。CBC块式加密模式,另外一类模式是流式加密
  • SHA,最后的完整性校验算法(哈希算法),保证密文不被篡改
  • 0xc013,密码套件的编号,每种密码套件都有独立编号。完整编号列表 IANA的网站

不同的客户端和服务端软件上,这些密码套件也各不同。TLS握手的重要任务之一就是 找到双方共同支持的那个密码套件,即“共同语言”,否则握手就必定会失败。

这案例排查下一步,就是搞清

客户端和服务端到底支持哪些Cipher Suite

客户端的密码套件有哪些?前面curl输出显示双方协商出来的是

ECDHE_RSA_AES_128_GCM_SHA256

但:

  • 这是协商后达成的结果,只是个套件,不是套件列表
  • 这密码套件是curl这客户端的,不是出问题的客户端

出问题的客户端:实际的业务代码去连接API server时的客户端,它是个Java库,而非curl。

咋获得这Java库能支持的密码套件列表?最直接的, 抓包分析。回到前面那抓包文件,检查Client Hello报文。在那就有Java库支持的密码套件列表:

找到客户端的密码套件列表了。

接下来,是不是找服务端的密码套件列表?不过,这抓包里,服务端直接回复了Alert消息,并未提供它支持的密码套件列表。排查如何推进?

换个思路

看服务端在TLS握手成功后用了哪个密码套件,而不是拿到它的全部列表。前面curl成功, 看curl那次协商出来的套件,看它是否被Java库支持,就能判定了。

我们要导出这次Client Hello里面的密码套件列表,可以这样做:选中Cipher Suite,右单击,选中Copy,在次级菜单中选中All Visible Selected Tree Items:

得到列表:

Cipher Suites (28 suites)
    Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 (0xc023)
    Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 (0xc027)
    Cipher Suite: TLS_RSA_WITH_AES_128_CBC_SHA256 (0x003c)
    Cipher Suite: TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256 (0xc025)
    Cipher Suite: TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256 (0xc029)
    Cipher Suite: TLS_DHE_RSA_WITH_AES_128_CBC_SHA256 (0x0067)
    Cipher Suite: TLS_DHE_DSS_WITH_AES_128_CBC_SHA256 (0x0040)
    Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA (0xc009)
    Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (0xc013)
    Cipher Suite: TLS_RSA_WITH_AES_128_CBC_SHA (0x002f)
    Cipher Suite: TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA (0xc004)
    Cipher Suite: TLS_ECDH_RSA_WITH_AES_128_CBC_SHA (0xc00e)
    Cipher Suite: TLS_DHE_RSA_WITH_AES_128_CBC_SHA (0x0033)
    Cipher Suite: TLS_DHE_DSS_WITH_AES_128_CBC_SHA (0x0032)
    Cipher Suite: TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA (0xc008)
    Cipher Suite: TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA (0xc012)
    Cipher Suite: TLS_RSA_WITH_3DES_EDE_CBC_SHA (0x000a)
    Cipher Suite: TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA (0xc003)
    Cipher Suite: TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA (0xc00d)
    Cipher Suite: TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA (0x0016)
    Cipher Suite: TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA (0x0013)
    Cipher Suite: TLS_ECDHE_ECDSA_WITH_RC4_128_SHA (0xc007)
    Cipher Suite: TLS_ECDHE_RSA_WITH_RC4_128_SHA (0xc011)
    Cipher Suite: TLS_RSA_WITH_RC4_128_SHA (0x0005)
    Cipher Suite: TLS_ECDH_ECDSA_WITH_RC4_128_SHA (0xc002)
    Cipher Suite: TLS_ECDH_RSA_WITH_RC4_128_SHA (0xc00c)
    Cipher Suite: TLS_RSA_WITH_RC4_128_MD5 (0x0004)
    Cipher Suite: TLS_EMPTY_RENEGOTIATION_INFO_SCSV (0x00ff)

确实没 ECDHE_RSA_AES_128_GCM_SHA256 套件。至此,能确认问题根因:因为这Java库和API server 2之间没找到共同密码套件,所以TLS握手失败。

根因找到,下步就是升级Java库,让双方能协商成功。

API server 1能兼容这相对旧的Java库,所以没问题。

这问题难吗?还好,对吧?因为我们一旦对协议本身有准确理解,很多问题就易被“看穿”。扎实的理论知识很重要。

4 案例:有效期内的证书为啥报告无效?

产品开发团队向运维团队报告问题:他们的应用在代码发布后,就无法正常访问一个内部HTTPS站点,报错:certificate has expired。

我们日常对证书都做自动更新处理,不该有“漏网之鱼”塞。然后手工检查这HTTPS站点证书,确定在有效期内,这报错真见鬼了!

既然是代码发布后新问题,认为和发布有关。这次确实有变更,会在客户端打开服务端证书校验的特性,而这特性在以前不打开。但这还无法解释,为啥客户端居然会认为,一个明明在有效期内的证书是过期。

“秀才遇到兵”,感觉“讲理”行不通,换个思路,不纠结在有效期问题。

类似前一案例,交叉验证推进排查。在这台客户端和另一台客户端,用OpenSSL向这HTTPS站点发起TLS握手。

结果:从另外一台客户端的OpenSSL去连接这HTTPS站点,也报告certificate has expired。

既然OpenSSL可复现,就可进一步检查!因为OpenSSL属OS命令,虽然我们不了解如何在Node.js debug,但对如何在OS排查有经验。

OpenSSL命令前加 strace,以便追踪OpenSSL执行过程中,特别在报告certificate has expired前到底发生啥:

strace openssl s_client -tlsextdebug -showcerts -connect abc.ebay.com:443

输出关键:

stat("/usr/lib/ssl/certs/a1b2c3d4.1", {st_mode=S_IFREG|0644, st_size=2816, ...}) = 0
openat(AT_FDCWD, "/usr/lib/ssl/certs/a1b2c3d4.1", O_RDONLY) = 6
......
write(2, "verify return:1n", 16verify return:1
)       = 16
.......
write(2, "verify error:num=10:certificate "..., 44verify error:num=10:certificate has expired
) = 44
write(2, "notAfter=", 9notAfter=)                = 9
write(2, "Oct 14 18:45:33 2020 GMT", 24Oct 14 18:45:33 2020 GMT) = 24
  • OpenSSL读取 /usr/lib/ssl/certs 下的文件 a1b2c3d4.1
  • 接着,OpenSSL就报告certificate has expired,expire日期2020年10月24日

明显进展:很可能就是这文件导致错误。它就是TLS客户端本地的Trust store里,存放的中间证书文件。Trust store一般存放根证书和中间证书文件,

5 TLS证书校验原理

一般,证书先存入文件系统,然后通过命令或代码,导入应用的Trust store。

5.1 TLS证书链

TLS证书验证是“链式”机制。如客户端存有根证书和它签发的中间证书,那由中间证书签发的叶子证书,就可被客户端信任了,也就是这样一条信任链:

信任根证书
     |
信任中间证书
     |
信任叶子证书

3种信任链:

  • case1、3,信任链完整,证书验证就可通过
  • case2,由于中间证书既不在客户端Trust store,也不在服务端回复的证书链,导致信任链断裂,验证失败

发现案例里,服务端发送的证书链包含正确的中间证书,为啥还失败?因为从前面strace openssl输出发现,客户端本地也有中间证书且 过期的:

这两张中间证书,签发机构是同一个CA,证书名称也相同,这就导致了OpenSSL在做信任链校验时,优先用了本地的中间证书,进而因为这张本地的中间证书确实已经过期,导致OpenSSL抛出了certificate has expired的错误!

你可能问:“照理说,叶子证书是新的中间证书签发的,用老的中间证书去验证叶子证书的签名的时候,应该会失败?”

没错,最烧脑的是:这两张中间证书,不仅签发机构一样,名称一样, 私钥也一样!

若你对TLS不熟,到这可能有点“爆炸”。核心在于:每次证书在更新时, 它对应的私钥不是必须要更新的,可保持不变。

我们把本地已过期的中间证书,称old_cert,新的中间证书称new_cert。整个故事:

  • 几年前,old_cert被根证书签发出来,名为inter-CA,保存在这台客户端的Trust store
  • 2020年,old_cert到期,根证书机构重新签发一张新的中间证书new_cert,它用新有效期,而 证书名称inter-CA 和对应 私钥 都不变
  • CA用这张new_cert签发这次的叶子证书。因为客户端程序没打开证书校验机制,所以没报错
  • 这天,新代码发布,证书校验机制被打开,于是客户端开始校验。发现这张叶子证书的签发者名称是inter-CA,而自己本地就有张也叫inter-CA的证书,于是尝试用这张证书的公钥去解开叶子证书的签名部分,可成功解开,于是确认old_cert就是对应的中间证书(而没用收到的new_cert,这是关键)。但由于old_cert已过期,结果客户端抛certificate has expired

5.2 TLS证书签名

TLS证书都有签名部分,这签名就是用签发者的私钥加密的。客户端为啥相信叶子证书真的是这CA签发的?因为客户端的Trust store里就有这CA的公钥(在CA证书里),它用这公钥去尝试解开签名,能成功,就说明这张叶子证书确是这CA签发。

最关键部分在于,新老中间证书用的私钥是同一把,所以这张叶子证书的签名部分, 用老的中间证书的公钥也能解开,使得下图中的橙色的验证链条“打通”,不过,谁也没料到打通一条“死胡同”。

PKI里有交叉签名的技术,就是新老根证书对同一个新的中间证书进行签名,但并不适用于这个案例。

OpenSSL报错原因找到了,根据这发现,也确认Node.js的Trust store也存在同样问题。把它的Trust store里过期证书全部删除,问题解决。Stack Overflow也有类似问题。

6 总结

  • 加密算法的类型

对称加密算法:加密和解密用同一个密钥,典型算法有AES、DES。

非对称加密算法:加密和解密用不同的密钥,典型的非对称加密算法有RSA、ECDSA。

  • TLS基础

TLS是先完成握手,然后进行加密通信。非对称算法用于交换随机数等信息,以便生成对称密钥;对称算法用于信息的加解密。

  • Cipher Suite

在握手阶段,TLS需要四类算法的参与,分别是:密钥交换算法、身份验证和签名算法、对称加密算法、消息完整性校验算法。这四类算法的组合,就形成了密码套件,英文叫Cipher Suite。这是TLS握手中的重要内容,我们的案例1就是因为无法协商出公用的密码套件,所以TLS握手失败了。

  • TLS证书链

TLS的信任是通过对证书链的验证:

信任根证书 -> 信任中间证书 -> 信任叶子证书

本地证书加上收到的证书,就形成了证书链,如果其中有问题,那么证书校验将会失败。我们的案例2,就是因为一些极端情况交织在一起,造成了信任链过期的问题,导致证书验证失败了。

  • Trust store

它是客户端使用的本地CA证书存储,其中的文件过期的话可能导致一些问题,在排查时可以重点关注。

  • 排查技巧

在排查技巧方面,你要知道使用 curl命令,检查HTTPS交互过程的方法:

curl -vk https://站点名

用 OpenSSL命令 来检查证书:

openssl s_client -tlsextdebug -showcerts -connect 站点名:443

需要分析OpenSSL为什么报错时,可加 strace,对排查根因有帮助。

如何在Wireshark里导出Cipher Suite的方法,就是在TLS详情中选中Cipher Suite,右单击,选中Copy,在次级菜单中选中All Visible Selected Tree Items。这时,列表就被复制出来了。

排查TLS Alert 40这个信息时,查阅 RFC5246 得到答案。遇到一些协议类型、定义相关的问题时, 最好查阅权威的RFC文档,获得最准确信息。

7 FAQ

TCP是三次握手,那么TLS握手是几次?

也是三次。TLS握手过程包括客户端发送ClientHello消息,服务器返回ServerHello消息和证书,客户端验证证书并发送加密所需的信息,服务器确认并发送加密所需的信息,最后客户端发送Finished消息,完成握手过程。其中前两步是服务器和客户端交换信息的第一次和第二次握手,后面的步骤是第三次握手。

假设服务端返回的证书链是根证书+中间证书+叶子证书,客户端没有这个根证书,但是有这个中间证书。你认为客户端会信任这个证书链吗?

如果客户端缺少根证书,那么客户端将无法验证证书链的完整性和真实性。在这种情况下,客户端将无法信任该证书链,即使客户端拥有中间证书。因此,为了建立可信的TLS连接,客户端必须拥有完整的证书链,包括根证书、中间证书和叶子证书。

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论