四、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 版本协商的审计点全流程

下面这张图把整个流程串起来了,红色框是必须记日志的关键位置。我每次给团队培训都会画一遍,因为它把“在哪里记”和“记什么”说得很清楚。

TLS 版本协商审计流程图 客户端发送 ClientHello 审计点①:记录 legacy_version 审计点②:记录 supported_versions 列表 服务端选择版本 服务端发送 ServerHello 审计点③:记录协商版本 审计点④:比对客户端 声称版本 vs 最终版本 记录密码套件 完成握手,日志归档 审计点汇总 ① legacy_version 原始值 ② supported_versions 列表 ③ 最终协商版本 + 密码套件

你看,整个流程其实不复杂。但问题是很多团队只在节点③记一次,完全不看①②。这就像考试只写答案不写过程——合规审查时,检查员一定会问“怎么证明服务端没有降级到不安全的版本?” 节点①②就是你的原始证据。

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 上报。被审计问过两次之后,我就再也不相信“交给云就完事了”的说法了。