四、TLS 版本协商参数映射归纳总结
TLS 版本协商,说白了就是客户端和服务器坐下来商量用哪个版本的协议。你想想看,双方握手的第一件事就是这个。审计日志里到底该记录哪些参数?我做过好几个合规项目,发现很多人漏掉了关键字段,后面被问起来才手忙脚乱地补。今天就把它彻底说清楚。
4.1 版本协商的核心参数
握手的第一步是 ClientHello,里面塞了一堆版本信息。我习惯把需要关注的点分成三组:
- 明文版本字段:ClientHello 中的
legacy_version,ServerHello 中的legacy_version。TLS 1.2 及以下版本,服务端直接用它作为最终版本。TLS 1.3 则用下面的扩展字段。 - supported_versions 扩展:从 TLS 1.3 开始引入。客户端列出一系列支持的版本列表,服务端从中选一个。如果没有这个扩展,就 fallback 到 legacy_version。
- 协商结果:服务端最终选择的版本号,会出现在 ServerHello 的
legacy_version或supported_versions扩展中(TLS 1.3 只记录在扩展里)。
我的经验:抓包时不看扩展字段只盯着 legacy_version,很容易误判。有一次我被客户质疑“为什么握手显示的是 TLS 1.2,但记录是 1.3?”,一看包——legacy_version 是 0x0303(TLS 1.2),但扩展里明确选了 1.3。审计日志必须以协商后的最终版本为准。
4.2 参数与审计日志字段映射
下面这张表是我在项目里反复打磨后的映射关系。记住:日志不能只记一个版本号,得把客户端声称的和服务器最终选的都写进去,这样才经得起合规查证。
| 握手阶段 | 原始参数 | 日志字段建议 | 说明 |
|---|---|---|---|
| ClientHello | legacy_version |
tls_client_legacy_version |
固定为 0x0303(TLS 1.2)或 0x0301(TLS 1.0) |
| ClientHello | supported_versions |
tls_client_supported_versions |
用逗号分隔的列表,如 "1.2,1.3" |
| ServerHello | legacy_version |
tls_server_legacy_version |
TLS 1.3 握手时此值固定为 0x0303 |
| ServerHello | supported_versions |
tls_negotiated_version |
最终协商版本,TLS 1.3 必须取自此处 |
| ServerHello | cipher_suite |
tls_negotiated_cipher |
建议同时记录,版本和密码套件往往一起审计 |
重点:tls_negotiated_version 才是合规审查的核心字段。凡是记录没有区分 “客户端声称” 和 “最终协商” 的,90% 会在审计中被打回来。
4.3 从代码中提取版本协商信息
如果你的应用是自己处理的 TLS(比如用 OpenSSL 或 BoringSSL),我建议直接钩住握手回调。下面这个伪代码思路,我在 Go 和 C 项目里都验证过:
// 伪代码示例:记录 TLS 版本协商
func onServerHello(conn, serverHello) {
// 从原始字节中解析
legacyVer := serverHello.legacy_version
negotiatedVer := serverHello.extensions.supported_versions
if negotiatedVer != nil {
// TLS 1.3 及以上
log.Field("tls_negotiated_version", formatVersion(negotiatedVer))
} else {
// TLS 1.2 及以下,用 legacy_version
log.Field("tls_negotiated_version", formatVersion(legacyVer))
}
// 同时记录客户端声称的版本(应该在之前的 ClientHello 解析时存下)
log.Field("tls_client_supported_versions", conn.clientHello.supportedVersions)
}
别小看这个分支判断。TLS 1.3 刚出来那会儿,很多人直接读 legacy_version,结果永远记成 0x0303,头疼了好久。
4.4 避坑指南 – 版本降级与中间人
我曾经遇到过一个问题:客户端声称支持 TLS 1.2 和 1.3,服务端却协商出 TLS 1.0。审计一看,怀疑有人做了版本降级攻击。怎么证明是客户端自己限制了,还是中间人篡改了?
解决方案:在审计日志里额外记录客户端发来的 supported_versions 原值(如果被抓包看到原值是 1.0 只有,那就是客户端的问题;如果原值是 1.2、1.3,但服务器回复 1.0,那就要查中间人或服务端策略了)。
警告:很多商用负载均衡器会在握手阶段修改 supported_versions,把客户端不支持的版本偷偷过滤掉。如果只记 final 版本,你永远不知道客户端原本想用什么。建议在日志里保留原始扩展的哈希或完整快照,方便事后取证。
4.5 版本协商的审计点全流程
下面这张图把整个流程串起来了,红色框是必须记日志的关键位置。我每次给团队培训都会画一遍,因为它把“在哪里记”和“记什么”说得很清楚。
你看,整个流程其实不复杂。但问题是很多团队只在节点③记一次,完全不看①②。这就像考试只写答案不写过程——合规审查时,检查员一定会问“怎么证明服务端没有降级到不安全的版本?” 节点①②就是你的原始证据。
4.6 日志样例格式推荐
最后我给出一个 JSON 格式的日志样例。记得版本字段用数字(比如 0x0304 表示 TLS 1.3),不要用字符串“1.3”,因为合规报告里经常要求原始字节值。当然你可以同时记录两个,方便不同角色阅读。
{
"timestamp": "2025-03-21T10:15:30.987Z",
"session_id": "abc123def456",
"tls_client_legacy_version": "0x0303",
"tls_client_supported_versions": ["0x0304", "0x0303", "0x0302"],
"tls_negotiated_version": "0x0304",
"tls_negotiated_cipher": "TLS_AES_128_GCM_SHA256",
"tls_handshake_duration_ms": 12
}
额外技巧:如果你用的是云厂商的 TLS 卸载,它们通常只暴露最终协商版本。我一般会要求云侧开一个“原始 ClientHello 日志”的开关,或者自己在客户端 SDK 里提前抓一份 supported_versions 上报。被审计问过两次之后,我就再也不相信“交给云就完事了”的说法了。