第3章:TLS记录协议——分片、压缩、加密与完整性校验

TLS 记录协议是整个安全通道的底层输出者。你想想看,应用层数据怎么变成一串加密字节在网络上跑的?全是记录协议干的活。我最早接触 TLS 时,以为就是直接加密,后来踩过坑才发现,分片、压缩(虽然现在基本不用了)、加密和完整性校验,每一步都有自己的坑。

3.1 分片——别让大包撑爆底层

应用层发来的数据可以是任意大小。但 TLS 记录协议对单个记录有上限:2¹⁴ 字节(16384字节)。超出就要切开。

  • 最大分片大小:标准值是 2¹⁴ 字节。但 TLS 1.2 允许客户端通过握手协商一个最大分片长度扩展(max_fragment_length)。
  • 最小分片大小:单个明文记录至少 1 字节。空记录只在特定场景出现(比如心跳或改变密码规格)。
  • 实际项目经验:有一次我接手一个老旧服务,SSL 加速卡只支持 2¹⁴ 字节的分片,但应用层网关会发 32KB 的 HTTP 响应,导致 TLS 层反复重分片,CPU 飙升。后来我把应用层的 TCP 缓冲区切到 16KB,问题直接解决。

如果你用 Nginx,可以调 ssl_buffer_size 来控制记录分片大小。默认是 16KB,适合大多数场景。

3.2 压缩——曾经有,现在没了

TLS 1.2 及之前版本支持可选压缩算法(如 DEFLATE)。但压缩发生在加密之前。你想想看,如果压缩后长度变化,攻击者可以利用 CRIME 攻击 通过观察密文长度猜测明文内容。

我记得 2012 年 CRIME 攻击爆出来时,好多 HTTPS 站点紧急关闭了 TLS 压缩。后来 TLS 1.3 干脆移除压缩支持。所以现在你遇到 TLS 1.3,就不用操这份心了。如果是 TLS 1.2,请务必确保压缩已禁用。

3.3 加密——从流密码到 AEAD

记录层加密算法的演进,说白了就是安全与性能的平衡史。

版本典型加密套件模式完整性
TLS 1.0/1.1TLS_RSA_WITH_RC4_128_MD5流密码 / CBC单独的 MAC (SHA1/MD5)
TLS 1.2TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256CBC (HMAC_then_Encrypt)分开的 HMAC
TLS 1.2 (现代)TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256AEAD (GCM)AEAD 自带认证
TLS 1.3TLS_AES_128_GCM_SHA256AEAD (GCM/CCM/ChaCha20-Poly1305)AEAD 认证 + HKDF

AEAD 模式(如 AES-GCM、ChaCha20-Poly1305)把加密和完整性校验合在一个操作里。不仅简化了实现,还避免了 CBC 模式中填充预言攻击(Padding Oracle Attack)。

不要在生产环境使用 CBC 模式的 TLS 套件。即使有 HMAC,BEAST 和 Lucky13 攻击依然能利用 CBC 的弱点。我见过一个金融系统因为用了 AES-128-CBC 被扫描器报高风险,改成 AES-128-GCM 就过了。

3.4 完整性校验——MAC 与 AEAD 的认证标签

在 TLS 1.2 及以前,记录层用单独的 HMAC 做完整性保护。加密前先计算 HMAC,然后 Encrypt(MAC || data)。接收方先解密,再验证 MAC。

这种“先 MAC 后加密”的顺序其实有隐患——如果解密失败,接收方可能泄漏错误信息,给攻击者提供 oracle。所以 TLS 1.3 强制使用 AEAD,认证标签在解密时一并验证,失败直接返回“解密失败”,不暴露任何细节。

我曾经在调试 TLS 1.2 连接时,抓包发现握手都成功了,但应用数据包一直报 bad_record_mac。查了两天才发现是两端协商的 HMAC 算法不一致——服务端用了 SHA256,客户端用了 SHA1,而密码套件里写的是 SHA384。嗯,这种配置错位在大型集群里很容易出现。

3.5 记录协议完整处理流程

下面这张图把发送端记录协议的处理流程串起来。我建议你把它存下来,面试时经常被问。

TLS 记录协议发送端流程 应用数据 分片(≤16KB) 压缩(TLS 1.3 移除) 加密 + 完整性 (MAC-then-Encrypt / AEAD) 添加记录头(5字节) 发送到 TCP 层 记录头结构:1字节类型 + 2字节版本 + 2字节长度

记录头只有 5 字节:ContentType (1B) + LegacyVersion (2B) + Length (2B)。接收方根据 Length 字段知道要读多少密文,然后解密、解压缩(如果开启)、解分片,最后拼回完整应用数据。

3.6 实战中常见的坑

  • 分片大小与 TCP MSS 不匹配:如果单个 TLS 记录大小超过 TCP 的 MSS(通常 1460 字节),会在 IP 层分片,增加丢包和重传概率。我建议 CN em>TLS 记录大小不要超过 TCP MSS + 开销。最佳实践是让应用层传递的数据略小于 1460 字节(减去 TLS 头开销后)。
  • 记录头的长度字段:接收方必须按照长度字段值读取精确字节数,否则解密时会出错。我曾经调试过一个客户端的 pull 问题,发现对方在分片边界处多读了一个字节,导致后续所有记录解密失败。
  • 压缩空包:即使压缩没数据,记录协议也会发送一个最小记录作为心跳或保活。如果防火墙配置了最小包长过滤,这种空包可能被丢弃。

在 TLS 1.2 中,如果你一定要使用非 AEAD 套件(其实我强烈不建议),注意不要暴露解密失败的详细信息。很多库默认打日志,日志里包含“BIO_read returned -1”这类信息,攻击者能据此实施 oracle 攻击。生产环境请关闭 DEBUG 级别的 TLS 日志。

记录协议是整个 TLS 的骨干。分片保证底层传输友好,压缩已被历史淘汰,加密和完整性校验从分离走向 AEAD 统一。当你下次碰到 TLS 连接异常时,先在记录层抓包看看——分片大小对不对、记录头有没有异常、密文长度是否合理——往往能快速定位问题。我个人习惯用 Wireshark 的“Follow TCP Stream”配合“TLS”过滤器,一眼就能看出问题记录。