第4章:TLS记录协议详解

前面我们把握手协议聊透了,那握手之后的数据传输靠什么?

就是记录协议。说白了,它是TLS的“运输层”,负责把应用层数据切碎、压缩、加密、加上完整性校验,再扔给TCP。反过来也一样,收到TCP报文后先解密、验证、解压缩、再组装成应用数据。

我个人习惯把记录协议看作一个流水线。每次处理一个记录,每个记录就是一个独立的处理单元。今天我们就把这个流水线拆开看。

4.1 记录协议的基本格式

每个TLS记录长这样:

字段 长度 说明
Content Type 1字节 记录类型:20=ChangeCipherSpec,21=Alert,22=Handshake,23=Application Data
Protocol Version 2字节 TLS版本,例如TLS 1.2是0x0303,TLS 1.3是0x0304
Length 2字节 负载长度(不超过2^14字节,即16384)
Fragment 可变 实际数据(加密后为密文+MAC/标签)

嗯,这里要注意:TLS 1.3之后版本字段被废弃,但结构里还留着,只是固定写0x0303。我以前审计过一个旧设备,发现它把版本字段写成了0x0300(SSL 3.0),直接判定为不合规。

4.2 记录的处理流程

我习惯把流程分成四个步骤。你想想看,这其实很像一个加工厂:

  1. 分段:应用层数据进来,先切成不超过16KB的片段。
  2. 压缩:可选步骤,TLS 1.3已废弃,但1.2及之前可能会用。我在项目里几乎没见过真开压缩的,反而因为CRIME/BREACH攻击,大家默认都关掉。
  3. 加密和完整性保护:这是核心。用当前会话的密钥对片段加密,同时计算消息认证码(MAC)或认证加密标签。
  4. 添加记录头:把类型、版本、长度等拼在最前面,形成完整记录。

关键点:加密前,TLS 1.2用“MAC-then-Encrypt”顺序;TLS 1.3则强制使用AEAD(如AES-GCM),一次完成加密和认证。这也是审计时最容易发现的版本差异。

TLS记录协议处理流程 应用层数据 分段(≤16KB) 压缩(可选) 加密+MAC 添加记录头(类型/版本/长度) 发送给TCP 说明:接收端是反向流程 先解析记录头 → 解密+验证MAC → 解压缩(如果压缩过)→ 重组 注意:TLS 1.3 强制AEAD,省略了MAC步骤

4.3 加密与完整性细节

这里水比较深,我重点说几个审计必查的点:

  • 序列号:每个方向(客户端→服务器、服务器→客户端)各自维护一个64位序列号,随每个记录递增。序列号参与MAC计算,防止重放。我曾经审计过一个自研TLS库,发现它在重置连接后没归零序列号,导致重放攻击可行。
  • MAC计算:TLS 1.2使用HMAC,输入为:序列号 + 记录类型 + 版本 + 长度 + 明文片段。所以即便同一个数据明文,在不同记录中MAC也不同。
  • 填充:块加密(如AES-CBC)需要填充到块大小。注意填充必须合法,TLS 1.2会严格校验,否则报bad_record_mac。我见过有人在实现时把填充字节任意填0,结果和另一端对不上。

审计小技巧:抓包时可以用Wireshark勾选“TLS记录”层,逐帧看Content Type和长度。如果看到Content Type=23但长度异常小(比如10字节),可能是不完整的应用数据帧,说明实现有bug。

4.4 TLS 1.3的变化

TLS 1.3对记录协议做了简化:

  • 移除了ChangeCipherSpec类型(虽然保留兼容,但实际不用)。
  • 所有加密记录都用AEAD,不再区分MAC和加密两个步骤。
  • 引入了“记录拖尾”概念,有些数据可能藏在已加密的警报或密钥更新里。

警告:TLS 1.3中,握手消息的加密发生在服务器发送Finished之后,之前的证书等以明文发送(但被签名保护)。审计时如果要检测1.3的合规性,需要特别注意记录类型为Handshake(22)且版本为0x0303的那些记录——它们可能是1.3的加密握手,而非1.2。

4.5 实战:用scapy解析一个TLS记录

有时候我要审计一个pcap文件,会用Python快速验证。下面是个简化的例子:

from scapy.all import *

# 读取第一个TLS记录(假设已经过滤出TCP payload)
pkt = IP(raw_packet)
tls = pkt[TCP].payload

# 解析记录头
content_type = tls[0]
version_major = tls[1]
version_minor = tls[2]
length = struct.unpack('!H', tls[3:5])[0]
print(f"Content Type: {content_type}")
print(f"Version: {version_major}.{version_minor}")
print(f"Payload Length: {length}")

# 如果是应用数据,实际数据需要解密,但可以看长度
if content_type == 23:
    encrypted = tls[5:5+length]
    print(f"Encrypted payload hex: {encrypted.hex()}")

当然,真正审计时还要检查是否使用了弱密码套件、序列号是否连续、MAC是否匹配。但至少这个代码能帮你快速看记录结构是否合法。

核心总结:记录协议不是什么高深的东西,就是一个带安全头的包容器。审计的关键就是:记录头字段是否正确?加密和完整性有没有按版本要求执行?序列号有没有泄漏或乱序? 抓住这三个点,大部分问题都能看出来。