3、TLS协议原理:握手协议、记录协议、密码套件、证书链验证

说实话,TLS 协议在嵌入式世界里,经常被当成一个「黑盒子」。很多人调个库、配个证书,能连上就完事了。但一旦遇到内存不够、握手失败、证书过期这种坑,不懂原理就只能干瞪眼。

这一章,咱们就把 TLS 拆开看看。我尽量用实战的视角来讲,而不是照搬 RFC。

3.1 TLS 协议栈的整体架构

TLS 分两层:底层是记录协议,上层是握手协议、告警协议和变更密码规范协议。你想想看,记录协议就像个快递员,负责把数据打包、加密、传输;握手协议则是谈判专家,负责商量用什么算法、交换密钥。

核心认知: 记录协议是「运输层」,握手协议是「控制层」。两者缺一不可。

TLS 协议栈结构 应用层(HTTP / MQTT / CoAP) TLS 握手协议 密钥协商 / 身份认证 TLS 记录协议 分片 / 加密 / 完整性 变更密码规范协议 告警协议 底层传输层(TCP / UDP / DTLS) 通常基于 TCP,DTLS 基于 UDP 记录协议为上层所有协议(握手、告警、应用数据)提供统一的加密传输服务

3.2 记录协议:加密传输的基石

记录协议干的事其实很简单:接收上层的数据,把它切成合适大小的片段,然后压缩(可选)、加 MAC、加密,最后加上头部发出去。反过来就是接收流程。

我在一个 STM32 项目里遇到过一个问题:记录协议的最大负载是 2^14 字节,也就是 16KB。但嵌入式设备的 RAM 往往只有几十 KB,如果一次接收一个 16KB 的记录,内存直接爆了。后来我强制限制了对端发送的记录大小,才解决。

记录协议格式

字段 长度 说明
Content Type 1 字节 22=握手, 23=应用数据, 21=告警, 20=变更密码规范
Protocol Version 2 字节 如 0x0303 表示 TLS 1.2
Length 2 字节 负载长度,最大 16384
Payload 可变 加密后的数据 + MAC + 填充(如适用)

实战技巧: 在嵌入式设备上,建议将接收缓冲区设置为 16KB + 256 字节的余量。我曾经因为缓冲区刚好 16KB,结果遇到一个带 256 字节填充的记录,直接丢包了。

3.3 握手协议:最复杂的部分

握手协议是整个 TLS 的核心。说白了,就是客户端和服务器互相确认身份、商量加密算法、交换密钥材料。TLS 1.2 的完整握手需要 2-RTT,也就是两次网络往返。

我记得第一次调通 TLS 握手时,抓包看了半天,发现客户端发完 ClientHello 后服务器没回。查了半天,原来是服务器不支持客户端列出的任何一个密码套件。嗯,这种坑踩过一次就记住了。

完整握手流程(TLS 1.2)

  1. ClientHello: 客户端发送支持的 TLS 版本、密码套件列表、随机数。
  2. ServerHello: 服务器选择版本和密码套件,发送自己的随机数。
  3. Certificate: 服务器发送证书链(通常是叶子证书 + 中间 CA)。
  4. ServerKeyExchange: 如果是 DHE 或 ECDHE 密钥交换,服务器发送密钥参数。
  5. ServerHelloDone: 服务器说「我说完了」。
  6. ClientKeyExchange: 客户端发送密钥材料(如预主密钥的密文或 DH 公钥)。
  7. ChangeCipherSpec: 双方切换加密参数。
  8. Finished: 双方发送握手消息的 MAC,验证握手没有被篡改。

注意: 在嵌入式设备上,Certificate 消息可能非常大(尤其是证书链较长时)。我曾经见过一个设备的证书链有 5 级,总大小超过 10KB,导致握手缓冲区溢出。建议在握手前先协商最大片段大小。

3.4 密码套件:算法组合的艺术

密码套件就是一组算法的组合。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: PRF(伪随机函数)和 HMAC 用 SHA-256。

嵌入式设备推荐密码套件

套件 密钥交换 加密 性能 安全性
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 ECDHE AES-128-GCM 高(硬件加速) 高
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 ECDHE AES-128-GCM 中 高
TLS_PSK_WITH_AES_128_CCM PSK AES-128-CCM 极高(无证书) 中(需安全分发密钥)

我的建议: 在资源受限的 MCU 上,优先使用 ECDHE + ECDSA + AES-128-GCM。ECDHE 提供前向安全性,ECDSA 的签名比 RSA 小得多,AES-128-GCM 在支持硬件加速的芯片上非常快。

3.5 证书链验证:信任的传递

证书链验证是 TLS 安全性的基石。简单来说,就是从一个信任的根证书开始,一级一级验证下级证书的签名,直到验证到服务器证书。

我曾经在项目里遇到一个诡异的问题:证书链验证总是失败,但用 OpenSSL 命令行验证却是成功的。查了两天,发现是嵌入式设备上没有加载中间 CA 证书,导致无法构建完整的证书链。嗯,这种问题在嵌入式环境里太常见了。

验证步骤

  1. 构建证书链: 从服务器证书开始,逐级向上找签发者,直到找到信任的根证书。
  2. 验证签名: 用上一级证书的公钥验证下一级证书的签名。
  3. 验证有效期: 检查每个证书的 notBefore 和 notAfter。
  4. 验证用途: 检查证书的 Key Usage 和 Extended Key Usage 是否允许 TLS 服务器认证。
  5. 检查吊销状态: 通过 CRL 或 OCSP 检查证书是否被吊销(可选但推荐)。

嵌入式避坑指南: 很多嵌入式 TLS 库默认不验证证书链,只验证叶子证书。我曾经见过一个设备,因为没验证中间 CA,被一个自签名的假证书给骗了。务必开启完整的证书链验证。

证书链验证流程图

证书链验证流程 根 CA 证书(信任锚) 预置在设备中 签发 中间 CA 证书 由根 CA 签发 签发 服务器证书(叶子) 由中间 CA 签发 验证过程 ① 用根 CA 公钥验证中间 CA 签名 → ② 用中间 CA 公钥验证服务器证书签名 ③ 检查每个证书的有效期和用途 → ④ 检查吊销状态(CRL/OCSP) ⑤ 验证服务器证书的域名是否匹配

3.6 实战中的常见问题

说了这么多理论,最后分享几个我在嵌入式项目里踩过的坑。

  • 握手超时: 嵌入式设备的网络环境往往不稳定。我建议将握手超时设置为 5-10 秒,并实现重试机制。
  • 内存碎片: 频繁的握手消息分配和释放会导致内存碎片。我习惯使用内存池来管理 TLS 缓冲区。
  • 时间不同步: 很多嵌入式设备没有 RTC 或 RTC 不准,导致证书有效期验证失败。可以考虑使用 NTP 同步时间,或者放宽时间验证的窗口。
  • 密码套件协商失败: 服务器和客户端支持的套件没有交集。我建议在客户端至少配置 3-5 个常用套件,并优先使用 ECDHE 系列。

个人习惯: 我在每个项目里都会写一个 TLS 握手日志模块,把握手过程中的关键信息(协商的套件、证书指纹、握手耗时)打印出来。调试的时候特别有用。