第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 记录的处理流程
我习惯把流程分成四个步骤。你想想看,这其实很像一个加工厂:
- 分段:应用层数据进来,先切成不超过16KB的片段。
- 压缩:可选步骤,TLS 1.3已废弃,但1.2及之前可能会用。我在项目里几乎没见过真开压缩的,反而因为CRIME/BREACH攻击,大家默认都关掉。
- 加密和完整性保护:这是核心。用当前会话的密钥对片段加密,同时计算消息认证码(MAC)或认证加密标签。
- 添加记录头:把类型、版本、长度等拼在最前面,形成完整记录。
关键点:加密前,TLS 1.2用“MAC-then-Encrypt”顺序;TLS 1.3则强制使用AEAD(如AES-GCM),一次完成加密和认证。这也是审计时最容易发现的版本差异。
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是否匹配。但至少这个代码能帮你快速看记录结构是否合法。
核心总结:记录协议不是什么高深的东西,就是一个带安全头的包容器。审计的关键就是:记录头字段是否正确?加密和完整性有没有按版本要求执行?序列号有没有泄漏或乱序? 抓住这三个点,大部分问题都能看出来。