签前面三个案例里的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:1\n", 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也有类似问题。
总结
- 加密算法的类型
对称加密算法:加密和解密用同一个密钥,典型算法有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文档,获得最准确信息。
FAQ
TCP是三次握手,那么TLS握手是几次?
也是三次。TLS握手过程包括客户端发送ClientHello消息,服务器返回ServerHello消息和证书,客户端验证证书并发送加密所需的信息,服务器确认并发送加密所需的信息,最后客户端发送Finished消息,完成握手过程。其中前两步是服务器和客户端交换信息的第一次和第二次握手,后面的步骤是第三次握手。
假设服务端返回的证书链是根证书+中间证书+叶子证书,客户端没有这个根证书,但是有这个中间证书。你认为客户端会信任这个证书链吗?
如果客户端缺少根证书,那么客户端将无法验证证书链的完整性和真实性。在这种情况下,客户端将无法信任该证书链,即使客户端拥有中间证书。因此,为了建立可信的TLS连接,客户端必须拥有完整的证书链,包括根证书、中间证书和叶子证书。