TLS 记录层协议结构

要说 TLS 里哪个层最容易被忽视,我第一个提名记录层。很多人盯着握手协议、证书链,觉得记录层就是“包个壳”。其实不对。记录层是 TLS 的骨架,所有上层数据——握手、告警、心跳、应用数据——最终都得装进它的“信封”里。你想想看,水印字段无论藏在握手扩展里、还是在应用数据 payload 里,都得从记录层过一遍。所以,不理解记录层结构,水印怎么“穿”过去的你都说不清楚。

这一节,我从记录层的基本格式讲起,再带你看看封装流程,最后聊聊和水印相关的几个坑。嗯,都是我当时踩过的。

2.1 记录层的位置与职责

TLS 协议栈分两层:上层是握手、告警、应用数据等协议;下层就是记录层(Record Protocol)。记录层负责:

  • 分段:上层数据太大?切成 2¹⁴ 字节(16 KB)以内的记录。
  • 压缩:可选,但现在的 TLS 1.3 已经废弃压缩。
  • 添加 MAC / 加密:先算完整性校验码(MAC),再加密(或 AEAD 同时完成)。
  • 添加头部:打好信封,交给 TCP。

我记得第一次 debug 一个奇怪的问题:服务端突然发了个 0 长度的应用数据记录,客户端直接报错。后来翻 RFC 才知道,记录层允许空记录,但很多实现会拒绝。这就是记录层细节的威力——一个字节都可能让链路断了。

避坑指南:记录层分片未必和上层消息边界对齐。一个 HTTP/2 frame 可能被切成两个 TLS 记录;同样,多个小握手消息可能合并进一条记录。水印检测时,如果按记录边界解读,就会漏掉跨记录的 watermark。

2.2 记录格式结构

每一条 TLS 记录都是一个 TLV(Type-Length-Value)结构。我直接给你看最经典的格式(TLS 1.2 及更早版本):

struct {
    ContentType type;
    ProtocolVersion version;
    uint16 length;
    opaque fragment[TLSPlaintext.length];
} TLSPlaintext;

各字段的含义、长度和限制,我整理成表格了:

字段 长度(字节) 说明 对水印的影响
type 1 内容类型:20=ChangeCipherSpec, 21=Alert, 22=Handshake, 23=Application Data 可被用于区分水印所在记录类型(例如藏在握手记录 vs 应用数据记录)
version 2 协议版本:TLS 1.0 (0x0301), 1.1 (0x0302), 1.2 (0x0303), 1.3 (0x0304 但用于 Record 层) 旧版本字段固定,可做水印载体的“时间戳”
length 2 负载长度(0 ~ 2^14-1),即 fragment 字节数 长度字段异常值可携带隐蔽信息,但易被检测
fragment 0 ~ 16384 加密前的明文负载,或加密后的密文(取决于记录层状态) 水印可嵌入在 fragment 中(握手扩展、应用数据 payload 等)

你看,ContentType 只有几个值,但攻击者可以反复发送特定类型来调制水印——这叫“类型调制水印”。我在做流量分析对抗项目时,就遇到过通过改变记录 type 来标记会话的技术:正常是 23,偶尔插一条 21(告警)来编码 0 或 1。嗯,防不胜防。

2.3 记录层封装流程

一条应用数据从发送到变成 TLS 记录,大致经历这几步。我画了个 SVG 图,你看一下:

TLS 记录层封装流程 应用数据 (如 HTTP 请求) 分段 (每段 ≤ 16KB) 压缩(可选) TLS 1.3 已禁用 添加 MAC (或 AEAD 标签) 加密 (AES-GCM / ChaCha20) 添加记录头 (Type + Version + Length) 发送到 TCP 水印通常嵌入在“应用数据”或 “握手扩展”阶段,但会被加密覆盖

流程你看到了:原始数据先分段,压缩(已废弃),加 MAC,加密,再套上头部。水印字段如果在应用数据明文里,经过加密后就会被隐藏。但如果在握手扩展、证书、或者通过记录类型调制,加密后仍然能看到类型字段——这就是为什么基于记录类型的水印能穿透 TLS。

2.4 记录层处理的几个细节

有朋友问过我:“记录层会不会把一条握手消息拆到两个记录里?”会的。而且不同实现策略不一样。OpenSSL 喜欢在握手阶段每条记录只发一个消息,但 NSS 可能会攒几个小消息合并成一条记录。这种差异在水印检测时有影响:如果你在水印嵌入时假设消息边界就是记录边界,那就可能错位。

我个人习惯是,做水印方案时永远不依赖记录边界。我更推荐在消息内部做冗余标记,比如重复某个扩展、或者通过消息顺序来编码。这样记录层怎么分片都不影响。

2.5 记录层对水印的隐蔽性影响

记录层带来的最大好处——也是最大的挑战——是 加密。加密后,除了 Type、Version、Length 这三个明文字段,剩下的 fragment 全是密文。水印如果放在 fragment 里,对手要看懂水印就必须先解密。而解密的密钥只有通信双方掌握,所以对手无法提取水印。这是“前向安全”水印的基础。

但是,Type 和 Length 字段是明文的。我曾经参与过一个项目,想要通过“在正常应用数据记录中插入告警记录”来传递隐蔽信息。这个方案很隐蔽,因为告警记录也可以有任意数据(Alert 的 description 字段)。但问题是,网络监控设备可以观察到异常的告警频率,比如每分钟只发一个“0x15”的告警,这就暴露了水印的存在。

注意:记录层头部无法被加密保护。ContentType 和 Length 字段是第 5 类暴露信息,可用于流量指纹、水印调制。如果你的水印方案依赖这两个字段,务必做抗检测设计,比如伪装成正常业务的统计分布。

2.6 实操小结

这一节内容其实很基础,但基础的东西往往最容易被忽略。我把核心要点列出来,你动手时可以对照着检查:

  1. TLS 记录层封装格式:Type(1 字节)、Version(2 字节)、Length(2 字节)、Fragment。
  2. 记录层处理顺序:分段 → 压缩(可选) → MAC → 加密 → 加头。
  3. 水印可嵌入位置:Fragment(被加密)、Type(明文)、Length(明文)、Version(明文)。
  4. 记录分片与合并会导致水印检测的错位问题,推荐在消息内部做标记。