3. TLS记录层协议精解
TLS 握手协商出的密钥与算法最终都要落到记录层(Record Layer)来执行。记录层是 TLS 的“运输封装层”:上层握手消息、告警、应用数据都被切成记录(record),逐条加密、加完整性校验,再交给 TCP 传输。理解记录层,等于理解了 TLS 在网络上真正“长什么样”。
记录层的封装结构
每条 TLS 记录由 5 字节头部和载荷组成。头部三个字段:ContentType(1 字节)、legacy_record_version(2 字节)、length(2 字节)。载荷长度上限为 2^14 = 16384 字节,这是 TLS 明文分片的上限。
ContentType 常见取值:20 表示 ChangeCipherSpec,21 表示 Alert,22 表示 Handshake,23 表示 Application Data。在 TLS 1.3 中,握手消息本身也走 23(Application Data)类型,靠内层握手头区分,这是与 TLS 1.2 的重要差异。版本字段在 TLS 1.3 中固定写 0x0303(TLS 1.2),仅为兼容中间设备,真实版本由 supported_versions 扩展协商。
分片、压缩与加密流程
发送方向的处理顺序是:分片 → 压缩(历史)→ 加 MAC → 加密。上层数据先被切成不超过 16384 字节的片段,每片独立成一条记录。压缩在 TLS 1.3 中已被彻底移除,TLS 1.2 虽保留但默认禁用,原因是 CRIME 等攻击利用压缩率泄露明文信息,因此现代实现一律不启用。
完整性保护有两种模式。TLS 1.2 的 AEAD 套件(如 AES-GCM)使用“先加密后认证”的 AEAD 结构,MAC 由 AEAD 内部完成;而 CBC 类套件使用 MAC-then-Encrypt,历史上易受 Lucky13 等时序攻击。TLS 1.3 只保留 AEAD,彻底删除了 CBC 与流密码,结构更干净。
序列号与防重放
每条记录都有一个 64 位序列号,从 0 开始,每发一条记录加 1,收发双方各自维护独立计数器。序列号不随记录传输,而是作为 AEAD 的 nonce 输入参与计算。接收方用自己维护的期望序列号解密,一旦不匹配就丢弃并告警。这样攻击者无法重放旧记录,因为重放记录的序列号与接收方期望值不符。序列号在密钥更新(KeyUpdate)后重置,配合新密钥使用。
用 Wireshark 观察记录层
抓包时若没有密钥,Wireshark 只能看到记录头,载荷显示为 “Encrypted Application Data”。要解密,需要客户端导出会话密钥。以浏览器为例,设置环境变量 SSLKEYLOGFILE 指向一个可写文件,浏览器会把每条连接的密钥材料追加写入。然后在 Wireshark 的 Preferences → Protocols → TLS 中指定该文件路径,即可自动解密。
# Linux/macOS 启动浏览器前设置
export SSLKEYLOGFILE=$HOME/tls_keys.log
# 验证文件生成
ls -l $HOME/tls_keys.log
# 文件内容形如:
# CLIENT_RANDOM <32字节hex> <主密钥hex>
解密后,在 Wireshark 过滤栏输入 tls.record.content_type == 23 可只看应用数据记录;输入 tls.record.length > 16000 可观察接近上限的大记录。展开一条记录,能看到 Content Type、Length、以及解密后的 TLS 内层结构。若看到多条 16384 字节记录连续出现,说明上层数据被分片传输,这是记录层分片的直接证据。
关键判断与常见误区
第一,序列号不传输,所以抓包看不到它,但解密后 Wireshark 会显示推导出的序列号。第二,TLS 1.3 的握手记录外层类型是 23,不要因为看到 Application Data 就以为握手已结束。第三,记录长度字段是密文长度,包含 AEAD tag(通常 16 字节),因此实际明文比 length 小 16 字节。第四,压缩字段虽在 TLS 1.2 规范中存在,但现代栈一律协商 null 压缩,看到非零压缩方法应视为异常。
掌握记录层后,后续分析证书、握手扩展、会话恢复时,都能准确判断“这条记录属于哪个阶段、是否被正确保护、序列号是否连续”,这是 TLS 排障与安全审计的基本功。