3. TLS 1.2 协议架构:四层协议栈

说实话,刚接触 TLS 的时候,我也被那一堆协议名字搞晕过。什么记录协议、握手协议,还有 Change Cipher Spec,听着就像四个不同的东西在打架。

但后来我明白了——TLS 1.2 本质上是一个分层协议栈。底层是记录协议,负责传输;上层是三个子协议,负责具体的业务逻辑。我习惯把这四层想象成一个快递系统:记录协议是货车,握手协议是签合同,警报协议是售后,Change Cipher Spec 是交接单。

核心要点:TLS 1.2 由四个子协议构成——记录协议(Record Protocol)、握手协议(Handshake Protocol)、警报协议(Alert Protocol)、Change Cipher Spec 协议。它们协同工作,共同保障通信安全。

TLS 1.2 四层协议架构图 TLS 1.2 协议栈架构 上层子协议(承载于记录协议之上) 握手协议 Handshake Protocol 警报协议 Alert Protocol Change Cipher Spec 协议 记录协议 Record Protocol(底层传输载体) TCP / UDP 传输层 说明:三个上层协议都通过记录协议封装后发送,记录协议负责分片、压缩、加密和MAC计算

3.1 记录协议——TLS 的底层运输工

记录协议是 TLS 1.2 的基石。所有上层协议的数据,最终都要交给记录协议来打包发送。我打个比方:握手协议要谈条件,警报协议要喊救命,但它们自己不会「说话」,得靠记录协议这个传话筒。

记录协议干的事情,说白了就四步:

  1. 分片——把上层数据切成不超过 2^14 字节(16KB)的小块。我在项目中遇到过一个大证书,握手消息超过 16KB,记录协议自动分成了两片发送。
  2. 压缩——可选步骤,但实际中几乎没人用。我记得 TLS 1.3 直接把它砍掉了,因为压缩容易引发安全漏洞(CRIME 攻击了解一下)。
  3. 添加 MAC——计算消息认证码,保证数据没被篡改。嗯,这里要注意:MAC 是在加密之前计算的,这叫「先认证后加密」。
  4. 加密——用协商好的对称密钥加密数据。加密后的密文加上记录头,扔给 TCP。

我的经验:调试 TLS 问题时,我最先看的就是记录协议层。用 Wireshark 抓包,过滤 tls.record,能看到每个记录的类型、版本、长度。如果记录长度异常,多半是分片逻辑出问题了。

记录协议的头部只有 5 个字节,结构如下:

字段 长度 说明
Content Type 1 字节 上层协议类型:20=ChangeCipherSpec, 21=Alert, 22=Handshake, 23=Application
Protocol Version 2 字节 TLS 版本号,TLS 1.2 为 0x0303
Length 2 字节 负载长度(加密后是密文长度)

你想想看,5 个字节的头就能区分四种上层协议,设计得挺精巧的。Content Type 字段的值 20-23,我到现在还记得清清楚楚——因为当年排查一个兼容性问题,就是靠这个字段定位的。

3.2 握手协议——TLS 的谈判专家

握手协议是 TLS 里最复杂的部分。它的任务就是:协商加密参数、验证对方身份、生成共享密钥。说白了,就是双方坐下来谈:「你用啥加密?我看看你的证书?好,咱俩算个共同密钥出来。」

握手协议本身又包含多个子消息,我列一下常见的:

  • ClientHello——客户端发起握手,告诉服务器自己支持的 TLS 版本、密码套件列表、随机数等。
  • ServerHello——服务器从客户端列表里选一套,回复自己的选择。
  • Certificate——服务器发送自己的证书链(可选客户端证书)。
  • ServerKeyExchange——携带 Diffie-Hellman 参数或 ECDHE 参数。
  • CertificateRequest——服务器要求客户端也发证书(双向认证时用)。
  • ServerHelloDone——服务器说「我说完了,该你了」。
  • ClientKeyExchange——客户端发送自己的密钥交换参数(如 DH 公钥)。
  • CertificateVerify——客户端用私钥签名,证明自己持有证书对应的私钥。
  • Finished——双方各自发送握手消息的 MAC,确认握手没有被篡改。

注意:Finished 消息是加密的!在发送 Finished 之前,双方已经通过 Change Cipher Spec 协议切换到了新密钥。我曾经见过一个 bug:客户端发了 Finished,但服务器解密失败——原因是密钥计算时随机数取错了。

握手协议的消息格式也很统一:

struct {
    HandshakeType msg_type;    // 1 字节,消息类型
    uint24 length;             // 3 字节,消息体长度
    opaque body<0..2^24-1>;   // 消息体
} Handshake;

每个握手消息前面都有 4 个字节的头(1 字节类型 + 3 字节长度)。这个设计让我觉得挺舒服的——解析起来非常规整,不像某些协议那样字段边界乱七八糟。

3.3 警报协议——TLS 的报警系统

警报协议很简单,就干一件事:报告错误和关闭连接。它只有两个字段:

字段 长度 说明
Level 1 字节 严重级别:1=Warning, 2=Fatal
Description 1 字节 具体错误码,如 40=handshake_failure, 42=bad_certificate

警报分两种级别:

  • Warning——非致命错误,连接可以继续。比如收到一个 close_notify 警告,表示对方要优雅地关闭连接了。
  • Fatal——致命错误,收到后必须立即断开连接。比如 bad_record_mac,说明数据被篡改过。

我个人习惯在代码里把警报日志打全。很多开发者只关注握手成功与否,忽略了警报信息。其实警报里藏着大量调试线索——比如 certificate_expired 告诉你证书过期了,unknown_ca 说明证书链没配全。

避坑指南:我曾经遇到一个线上问题,客户端连不上服务器,抓包发现服务器发了 handshake_failure 警报。查了半天,原来是服务器密码套件列表里没有客户端支持的套件。所以,看到 handshake_failure 先检查密码套件兼容性。

3.4 Change Cipher Spec 协议——密钥切换开关

这个协议可能是四个里最简单的了。它就一个字节:0x01。表示「从现在开始,我要用新协商的密钥加密了」。

你可能会问:为什么需要单独一个协议?直接在手握消息里带个标志不行吗?

嗯,这里有个历史原因。Change Cipher Spec 的存在是为了明确划分密钥切换的边界。在 TLS 1.2 中,握手过程是:

  1. 双方交换 Hello 和密钥参数(明文)
  2. 服务器发送 Change Cipher Spec(明文)
  3. 服务器发送 Finished(用新密钥加密)
  4. 客户端发送 Change Cipher Spec(明文)
  5. 客户端发送 Finished(用新密钥加密)

Change Cipher Spec 本身不加密,但它之后的第一个记录就必须用新密钥加密。这个设计虽然有点冗余,但胜在清晰——接收方看到 Change Cipher Spec,就知道下一个记录要换密钥解密了。

关键点:Change Cipher Spec 协议在 TLS 1.3 中被移除了,因为 TLS 1.3 重新设计了握手流程,不再需要这个显式的切换信号。但在 TLS 1.2 中,它仍然是不可或缺的一环。

3.5 四个协议的协作流程

说了这么多,四个协议到底怎么配合?我画个简化的流程:

客户端                             服务器
  |                                  |
  |--- ClientHello (握手协议) ------>|
  |                                  |
  |<-- ServerHello (握手协议) -------|
  |<-- Certificate (握手协议) -------|
  |<-- ServerHelloDone (握手协议) ---|
  |                                  |
  |--- ClientKeyExchange (握手协议)->|
  |--- ChangeCipherSpec ------------>|  ← 切换密钥
  |--- Finished (加密) ------------->|
  |                                  |
  |<-- ChangeCipherSpec -------------|  ← 切换密钥
  |<-- Finished (加密) --------------|
  |                                  |
  |========= 安全通道建立 ===========|
  |                                  |
  |--- Application Data (记录协议) ->|
  |<-- Application Data (记录协议) --|
  |                                  |
  |--- Alert: close_notify --------->|  ← 优雅关闭
  |<-- Alert: close_notify ----------|

你看,整个流程里,握手协议负责「谈条件」,Change Cipher Spec 负责「拍板切换」,记录协议负责「运输所有数据」,警报协议负责「处理异常」。各司其职,井井有条。

我在做性能优化时发现,很多开发者只关注握手协议的耗时,忽略了记录协议的分片策略。其实记录协议的分片大小直接影响网络效率——分片太小,头部开销大;分片太大,丢包重传成本高。默认 16KB 是个折中值,但如果你在物联网场景下,建议调小分片。

重要提醒:警报协议中的 close_notify 不是可选的!TLS 规范要求,关闭连接前必须发送 close_notify 警报。不发送就断开,属于截断攻击。我曾经审计过一个项目,发现他们直接 close socket 不发送 close_notify——这会导致对端无法区分是正常关闭还是攻击。

好了,四个协议都讲完了。记住一句话:记录协议是路,握手协议是导航,Change Cipher Spec 是收费站,警报协议是交警。理解了它们的分工,TLS 1.2 的架构你就掌握了一半。