3、TLS协议原理:握手协议、记录协议、密码套件、证书链验证
说实话,TLS 协议在嵌入式世界里,经常被当成一个「黑盒子」。很多人调个库、配个证书,能连上就完事了。但一旦遇到内存不够、握手失败、证书过期这种坑,不懂原理就只能干瞪眼。
这一章,咱们就把 TLS 拆开看看。我尽量用实战的视角来讲,而不是照搬 RFC。
3.1 TLS 协议栈的整体架构
TLS 分两层:底层是记录协议,上层是握手协议、告警协议和变更密码规范协议。你想想看,记录协议就像个快递员,负责把数据打包、加密、传输;握手协议则是谈判专家,负责商量用什么算法、交换密钥。
核心认知: 记录协议是「运输层」,握手协议是「控制层」。两者缺一不可。
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)
- ClientHello: 客户端发送支持的 TLS 版本、密码套件列表、随机数。
- ServerHello: 服务器选择版本和密码套件,发送自己的随机数。
- Certificate: 服务器发送证书链(通常是叶子证书 + 中间 CA)。
- ServerKeyExchange: 如果是 DHE 或 ECDHE 密钥交换,服务器发送密钥参数。
- ServerHelloDone: 服务器说「我说完了」。
- ClientKeyExchange: 客户端发送密钥材料(如预主密钥的密文或 DH 公钥)。
- ChangeCipherSpec: 双方切换加密参数。
- 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 证书,导致无法构建完整的证书链。嗯,这种问题在嵌入式环境里太常见了。
验证步骤
- 构建证书链: 从服务器证书开始,逐级向上找签发者,直到找到信任的根证书。
- 验证签名: 用上一级证书的公钥验证下一级证书的签名。
- 验证有效期: 检查每个证书的 notBefore 和 notAfter。
- 验证用途: 检查证书的 Key Usage 和 Extended Key Usage 是否允许 TLS 服务器认证。
- 检查吊销状态: 通过 CRL 或 OCSP 检查证书是否被吊销(可选但推荐)。
嵌入式避坑指南: 很多嵌入式 TLS 库默认不验证证书链,只验证叶子证书。我曾经见过一个设备,因为没验证中间 CA,被一个自签名的假证书给骗了。务必开启完整的证书链验证。
证书链验证流程图
3.6 实战中的常见问题
说了这么多理论,最后分享几个我在嵌入式项目里踩过的坑。
- 握手超时: 嵌入式设备的网络环境往往不稳定。我建议将握手超时设置为 5-10 秒,并实现重试机制。
- 内存碎片: 频繁的握手消息分配和释放会导致内存碎片。我习惯使用内存池来管理 TLS 缓冲区。
- 时间不同步: 很多嵌入式设备没有 RTC 或 RTC 不准,导致证书有效期验证失败。可以考虑使用 NTP 同步时间,或者放宽时间验证的窗口。
- 密码套件协商失败: 服务器和客户端支持的套件没有交集。我建议在客户端至少配置 3-5 个常用套件,并优先使用 ECDHE 系列。
个人习惯: 我在每个项目里都会写一个 TLS 握手日志模块,把握手过程中的关键信息(协商的套件、证书指纹、握手耗时)打印出来。调试的时候特别有用。