作者归档:admin

由 Python 偶现 SSL 错误引发的 TLS cipher 问题探究

近期在用 Python (v3.12.0) 对某 API 接口持续性地调用测试过程中,发现会偶现 SSL 错误:

raise SslHandShakeException("ssl error", reason=e)
somesdk.exception.exceptions.SslHandShakeException: SslHandShakeException - ssl error
reason: SSLError(SSLError(1, '[SSL: SSLV3_ALERT_HANDSHAKE_FAILURE] sslv3 alert handshake failure (_ssl.c:1002)'))

由于拨测是在 Windows 云主机上进行,把脚本拉到本地后在本地重新安装 SDK 等相关 py 环境,发现问题依旧。

此前从同事那里得知 ipv6 网络环境曾出现过不稳定的情况,初步怀疑是是 ipv6 引起此问题,是否在走与不走 ipv6 链路的区别中产生了 “不稳定” 现象。把 Windows 系统的 ipv4/ipv6 分别禁用,排除了这种可能性。祭出 Wireshark 在本地抓包,同时不停地运行 Python 调用 API 脚本发送请求,终于在一段时间测试后发现了问题:API 所在主机有 A 、 B 两个 ipv4 解析结果,在解析结果为 B 时稳定复现此问题,解析结果为 A 时则稳定不出现问题。

由于 Python 的底层运行库提示为 SSL 错误,因此这两个 server 的 TLS 支持情况成为关键。此前从一些网络技术文章可知,对于客户端 TLS 指纹(包括但不限于 client cipher suite 的组合特征)、甚至 HTTP2 指纹的识别早已经应用到脚本、爬虫等 “非人” 客户端的识别封禁等。

调查 Python 客户端与 A 主机 TLS 握手失败的包,发现以下现象:在服务端返回握手失败包之前客户端所发送的 Client Hello 包中,客户端(Python3.12.0 及相关网络库)向服务端提供了以下 18 个加密套件:

Pyhton 3.12.0 提供的 18 个 cipher suite(点击展开)
1TLS_AES_256_GCM_SHA384(0x1302)
2TLS_CHACHA20_POLY1305_SHA256(0x1303)
3TLS_AES_128_GCM_SHA256(0x1301)
4TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384(0xc02c)
5TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384(0xc030)
6TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256(0xc02b)
7TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256(0xc02f)
8TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256(0xcca9)
9TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256(0xcca8)
10TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384(0xc024)
11TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384(0xc028)
12TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256(0xc023)
13TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256(0xc027)
14TLS_DHE_RSA_WITH_AES_256_GCM_SHA384(0x009f)
15TLS_DHE_RSA_WITH_AES_128_GCM_SHA256(0x009e)
16TLS_DHE_RSA_WITH_AES_256_CBC_SHA256(0x006b)
17TLS_DHE_RSA_WITH_AES_128_CBC_SHA256(0x0067)
18TLS_EMPTY_RENEGOTIATION_INFO_SCSV(0x00ff)
前三个属于 TLS1.3 的加密套件在本次实际交互中不被支持,最后一个 SCSV 不是真正的加密套件,只有剩下的 14 个可用,而这 14 个套件皆为 DHE 或 ECDHE 。
使用 sslscan 工具分别扫描 A 、 B 两台服务端所支持的 cipher 情况(点击展开)
A 支持的 cipher
TLSv1.2128 bits0xC013ECDHE-RSA-AES128-SHA
TLSv1.2256 bits0xC028ECDHE-RSA-AES256-SHA384
TLSv1.2256 bits0x0035AES256-SHA
TLSv1.2128 bits0x002FAES128-SHA
B 支持的 cipher
TLSv1.2256 bits0x0035AES256-SHA
TLSv1.2128 bits0x002FAES128-SHA

可见 Python 3.12.0 这样的 Client 在 TLS 握手时向服务端提供的加密套件中,和 B 没有交集,和 A 有交集 TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384,HEX 代码为 (0xc028),因此只能和 A TLS 握手成功。

在和其他部门同事复现、调试的过程中,据称同事的 Python3.6 没有这个问题,因此使用了自己另一台电脑的 Python3.9.2 环境进行复测,Python3.9.2 的相关 SSL 库在 TLS 握手 Client Hello 时提供了 43 个 cipher,最终服务端 B 在 Server Hello 中回复商定了 TLS_RSA_WITH_AES_256_CBC_SHA (0x0035) 作为加密套件。

Pyhton 3.9.2 提供的 43 个 cipher suite(点击展开)
1TLS_AES_256_GCM_SHA384(0x1302)
2TLS_CHACHA20_POLY1305_SHA256(0x1303)
3TLS_AES_128_GCM_SHA256(0x1301)
4TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384(0xc02c)
5TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384(0xc030)
6TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256(0xc02b)
7TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256(0xc02f)
8TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256(0xcca9)
9TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256(0xcca8)
10TLS_DHE_RSA_WITH_AES_256_GCM_SHA384(0x009f)
11TLS_DHE_RSA_WITH_AES_128_GCM_SHA256(0x009e)
12TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256(0xccaa)
13TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8(0xc0af)
14TLS_ECDHE_ECDSA_WITH_AES_256_CCM(0xc0ad)
15TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8(0xc0ae)
16TLS_ECDHE_ECDSA_WITH_AES_128_CCM(0xc0ac)
17TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384(0xc024)
18TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384(0xc028)
19TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256(0xc023)
20TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256(0xc027)
21TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA(0xc00a)
22TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA(0xc014)
23TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA(0xc009)
24TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA(0xc013)
25TLS_DHE_RSA_WITH_AES_256_CCM_8(0xc0a3)
26TLS_DHE_RSA_WITH_AES_256_CCM(0xc09f)
27TLS_DHE_RSA_WITH_AES_128_CCM_8(0xc0a2)
28TLS_DHE_RSA_WITH_AES_128_CCM(0xc09e)
29TLS_DHE_RSA_WITH_AES_256_CBC_SHA256(0x006b)
30TLS_DHE_RSA_WITH_AES_128_CBC_SHA256(0x0067)
31TLS_DHE_RSA_WITH_AES_256_CBC_SHA(0x0039)
32TLS_DHE_RSA_WITH_AES_128_CBC_SHA(0x0033)
33TLS_RSA_WITH_AES_256_GCM_SHA384(0x009d)
34TLS_RSA_WITH_AES_128_GCM_SHA256(0x009c)
35TLS_RSA_WITH_AES_256_CCM_8(0xc0a1)
36TLS_RSA_WITH_AES_256_CCM(0xc09d)
37TLS_RSA_WITH_AES_128_CCM_8(0xc0a0)
38TLS_RSA_WITH_AES_128_CCM(0xc09c)
39TLS_RSA_WITH_AES_256_CBC_SHA256(0x003d)
40TLS_RSA_WITH_AES_128_CBC_SHA256(0x003c)
41TLS_RSA_WITH_AES_256_CBC_SHA(0x0035)
42TLS_RSA_WITH_AES_128_CBC_SHA(0x002f)
43TLS_EMPTY_RENEGOTIATION_INFO_SCSV(0x00ff)

那么 Python3.12.0 和 Python3.9.2 的 Clinet cipher 到底有什么区别?略一对比可以看出 Python3.12.0 抛弃了相当多的 RSA 相关加密套件,而保留了和 DHE 、 ECDHE 相关的。

TLS1.3 一项重要的改进就是抛弃了 RSA 相关算法而保留了 Diffie–Hellman(DH)作为唯一的密钥交换算法。因为前者不具有前向安全性(Forward Secrecy,简称 FS),简单说,把 https 密文保存下来后,即使在未来私钥泄露的情况下,也无法解开所保存的密文。

Python3.10 的更新日志 中有一条:
The ssl module now has more secure default settings. Ciphers without forward secrecy or SHA-1 MAC are disabled by default. Security level 2 prohibits weak RSA, DH, and ECC keys with less than 112 bits of security. SSLContext defaults to minimum protocol version TLS 1.2. Settings are based on Hynek Schlawack’s research. (Contributed by Christian Heimes in bpo-43998.)
其思路和 TLS1.3 抛弃 RSA 比较一致,把不支持前向安全的加密套件废除以提高安全性。

最终服务端 B 包括域名的 v6 地址所接受的 cipher 与 A 拉齐,问题得到解决。一个看似简单又有点奇怪的 “SSL 偶现报错” 问题原来与 DNS 解析负载均衡、新版 Python 严格的 TLS 加密套件策略等一系列原因有关。

香港国际机场中转过夜

长假后的几天从东京成田 NRT 归国飞上海浦东 PVG,因为直飞过于离谱的价格,不得已选择了 HX 香港航空绕行香港机场 HKG,后半夜到达 HKG 后要逗留近 6 个小时,再在早晨转乘前往 PVG 的联程航班。更加不巧的是,10 月 9 日晚遇到了台风小犬,更是自 1992 年设立暴雨警告信号系统以来,首次出现 “八号风球” 与 “黑色暴雨警告” 同时生效的情况。

之前做了一些在 HKG 过夜的攻略,也咨询了在 HKG 过过夜的盆友 @xierch ,比如在什么样椅子上休息、充电插口有哪些、夜里去哪里吃东西等等,更高阶一点的可以考虑免费洗澡间和 24h 休息室。但是亲身体验一把之前心里还是觉得很没底的。

8 日晚在从 NRT 起飞前,香港天文台就已经挂出了九号风球,但这也没影响飞 HKG 的前序航班正常抵达。在降落前,飞机在风暴中忽上忽下的搏斗和机长颇有几分坚毅口吻的降落广播都难免使我有点紧张。 9 日 1:42(UTC+8) 终于落地,只见风雨如晦,八号风球仍正生效中。

进入航站楼后查看地图,发现位于 T1 的西翼,西翼也有转机点可上达出发层,但我事先已得知免费洗澡间在到达层,所以先不急上楼。在到达层向东翼走了茫茫长的一段路,也见到了许多在到达层下机口和衣而卧的人。在 12 号下机口发现洗澡间,但男浴室却高挂维护牌,女浴室则没有。一路上比肩继踵的人都奔向入境口,似乎也没见什么人对这两间澡堂子有兴趣的。

通过转机安检后上楼,来到了熟悉的 T1 东翼出发层。在免税店和登机口的交界处发现了 24h 麦当劳,进去(实际上是下去,这个店的平面位于出发层以下)后发现转机食饭的旅客挺多,更多的是身着各类制服的航司、机场工作人员在抓紧时间吃夜宵。可用点餐机,支微卡付款都支持。

匆匆吃完后来到真·冷冷清清的 T1 出发层,香港公共场所的冷气量妇孺皆知,在这种暴风雨天气下更是和我之前至少 9 次因转机等造访 HKG 留下的繁忙、热闹印象完全不同。冷冰冰的八号风球警告和空中花园关闭通知在广播中一遍遍地播送,更是给人多感官上都带来了不安。

从落地的时间算起,滑行、走路、吃饭已经花去了相当多的时间,吃完麦当劳后我记得已经大约快要凌晨三点。没什么心情再欣赏 HKG 的设施、飞行器等,直接在东翼的几个登机口找躺卧的地方,这里很多攻略 app 上都会列出详尽的躺椅、三连座的细目表,不过我并没有找到三连座的椅子,最终选择在 26 号登机口面向玻璃幕墙的躺椅安顿下来,放好了行李小推车,把港版转换头接上 GaN 充电器开始充电——这里不得不提到 HKG 虽然把 USB 充电口升级成了 USBA+C 这样的双口充电模块,但实际上这类插口的故障率仍然很高,我也没有条件去检测输出功率等情况,如果用自带的充电头,那么港版的转换头又少不了,着实有点麻烦啊。

即使是自带了专为过夜准备的毛毯,HKG 冷气的杀伤力还是不容小觑,右肩被吹得人根本睡不着,只能看着雨水在昏暗的停机坪照明下彻夜擦着玻璃。就这么干熬了一两个小时,约凌晨 6 点的时候 T1 已经热闹了起来,登机口也逐渐变得繁忙。找了个洗手间刷了牙,此时得知我往 PVG 航班的登机口被指派到了 5XX 摆渡车专用层,摇头苦笑,经历了如此难忘的一夜后,在大雨中露天上飞机好像都不算什么了。