4. TLS 1.2 协议详解:记录、握手、警报与密码套件

好,咱们今天来啃 TLS 1.2 这块硬骨头。说实话,TLS 协议栈看起来层数多,但核心就四个部分:记录协议、握手协议、警报协议,以及贯穿始终的密码套件。我当年刚接触时,总觉得握手协议最复杂,后来才发现,不理解记录协议,握手协议也白搭。

咱们一层层剥开看。

4.1 TLS 记录协议:最底层的搬运工

记录协议是 TLS 的基石。所有上层数据——握手消息、警报、应用数据——最终都要封装成记录,再交给 TCP 发送。

每条记录长这样:

struct {
    ContentType type;       // 内容类型(20=ChangeCipherSpec, 21=Alert, 22=Handshake, 23=Application Data)
    ProtocolVersion version; // 版本(TLS 1.2 为 0x0303)
    uint16 length;          // 负载长度
    opaque fragment[TLSPlaintext.length]; // 实际数据
} TLSPlaintext;

注意,记录协议本身不加密。加密是在记录层之上,由 ChangeCipherSpec 消息触发后,后续记录才用协商好的密钥加密。我见过有人调试时抓包看到明文握手,以为没加密——其实那是正常的,握手阶段本身就不加密。

我的习惯:调试 TLS 问题时,先看记录头里的 ContentType。如果握手过程中突然出现 21(Alert),那基本就是出错了。省得你从头到尾读十六进制。

4.2 TLS 握手协议:建立信任的谈判桌

握手协议是 TLS 最复杂的部分。说白了,就是客户端和服务器互相确认身份、协商密钥的过程。TLS 1.2 的握手流程,我拆成四个阶段来讲。

4.2.1 第一阶段:Hello 与能力协商

客户端先发 ClientHello,带上自己支持的 TLS 版本、密码套件列表、随机数等。服务器回复 ServerHello,选定版本和套件,也发一个随机数。

这里有个坑:客户端可能同时支持 TLS 1.2 和 1.3,但服务器只支持 1.2。如果客户端发的版本号是 1.3,服务器会拒绝吗?不会。TLS 的版本协商机制允许客户端发一个较高的版本,服务器选自己能支持的最高版本。我遇到过某款嵌入式设备,固件里写死了 ClientHello 版本为 1.2,结果升级后的服务器只认 1.3 的扩展字段——握手失败。嗯,兼容性测试一定要做。

4.2.2 第二阶段:服务器证书与密钥交换

服务器发 Certificate 消息,把自己的证书链发给客户端。接着发 ServerKeyExchange(如果需要的话),最后发 ServerHelloDone 表示“我说完了”。

什么时候需要 ServerKeyExchange?当使用 DHE 或 ECDHE 密钥交换时,服务器需要把 DH 参数发给客户端。如果是 RSA 密钥交换,这一步可以省略。我建议你优先用 ECDHE,前向安全性更好。

4.2.3 第三阶段:客户端证书与密钥交换

客户端收到服务器消息后,先验证服务器证书。验证通过后,发 ClientKeyExchange 消息。如果是 RSA 套件,这里传的是用服务器公钥加密的预主密钥;如果是 DHE/ECDHE,这里传的是客户端的 DH 参数。

然后客户端发 ChangeCipherSpec,告诉服务器“我要开始加密了”。最后发 Finished 消息,里面是对整个握手过程的 MAC 校验。

我曾经踩过的坑:在资源受限的 MCU 上,证书链验证非常耗时。有一次我忘了做证书链的路径长度限制,结果客户端收到一个 5 级深度的证书链,RAM 直接爆了。后来我强制限制了证书链最大深度为 3 级。

4.2.4 第四阶段:服务器完成

服务器也发 ChangeCipherSpec 和 Finished。至此,握手完成,双方开始用协商好的密钥加密应用数据。

整个握手流程,我用一张图来总结:

TLS 1.2 握手流程 客户端 服务器 ① ClientHello(版本、套件列表、随机数) ② ServerHello(选定版本、套件、随机数) ③ Certificate(服务器证书链) ④ ServerKeyExchange(DH 参数,可选) ⑤ ServerHelloDone ⑥ ClientKeyExchange(预主密钥或 DH 参数) ⑦ ChangeCipherSpec(开始加密) ⑧ Finished(握手校验) 握手完成后 开始加密通信

4.3 TLS 警报协议:优雅的错误处理

警报协议很简单,就两种级别:Warning 和 Fatal。Fatal 级别的警报会立即终止连接。

常见的警报类型:

警报编号 名称 说明
20 bad_record_mac 记录 MAC 校验失败,通常是密钥不匹配或数据被篡改
21 decryption_failed 解密失败(TLS 1.2 中已弃用,但有些实现仍会发)
22 record_overflow 记录长度超过 2^14+2048 字节
40 handshake_failure 握手失败,比如没有共同的密码套件
42 bad_certificate 证书无效(过期、签名错误等)
70 protocol_version 版本不兼容

我在调试时,最喜欢看警报消息。有一次设备连不上服务器,抓包发现服务器发了 handshake_failure(40)。查了半天,原来是设备只支持 RSA 密钥交换,但服务器只开了 ECDHE。改一下密码套件配置就好了。

关键点:嵌入式设备资源有限,建议只实现必要的警报类型。但 bad_record_mac 和 handshake_failure 必须支持,否则出问题你都不知道为什么。

4.4 TLS 密码套件:加密算法的组合拳

密码套件决定了 TLS 连接用什么算法。TLS 1.2 的套件命名格式是:

TLS_密钥交换算法_认证算法_WITH_加密算法_消息认证码算法

举个例子:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256

  • ECDHE:密钥交换使用椭圆曲线 Diffie-Hellman 临时密钥
  • RSA:证书认证使用 RSA 签名
  • AES_128_GCM:加密使用 128 位 AES 的 GCM 模式
  • SHA256:消息认证码使用 SHA-256

常见的密码套件对比:

套件名称 密钥交换 加密 MAC 前向安全 嵌入式推荐度
TLS_RSA_WITH_AES_128_CBC_SHA RSA AES-128-CBC SHA-1 否 不推荐(无前向安全)
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 ECDHE AES-128-GCM SHA-256 是 强烈推荐
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 ECDHE AES-128-GCM SHA-256 是 推荐(需 ECC 证书)
TLS_PSK_WITH_AES_128_CCM PSK AES-128-CCM CCM 内建 否 适合资源极受限设备

我个人习惯在嵌入式项目里优先用 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256。为什么?

  • ECDHE 提供前向安全性,即使私钥泄露,历史会话也无法解密
  • AES-128-GCM 是 AEAD 算法,加密和认证一步到位,省掉了单独的 MAC 计算
  • SHA-256 比 SHA-1 安全,而且很多 MCU 有硬件加速
注意:不要用 TLS_RSA_WITH_AES_128_CBC_SHA。RSA 密钥交换没有前向安全性,而且 CBC 模式容易受 Padding Oracle 攻击。我见过一些老设备还在用这个套件,赶紧换掉。

如果设备资源实在紧张,比如只有几十 KB RAM,可以考虑 PSK 套件。PSK 不需要证书,双方预共享一个密钥就行。但 PSK 没有前向安全性,而且密钥管理是个麻烦事。

好了,TLS 1.2 的四个核心协议就讲到这里。记住:记录协议是基础,握手协议是核心,警报协议是调试利器,密码套件是安全基石。把这四个搞明白,TLS 1.2 你就掌握了八成。