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 图,你看一下:
流程你看到了:原始数据先分段,压缩(已废弃),加 MAC,加密,再套上头部。水印字段如果在应用数据明文里,经过加密后就会被隐藏。但如果在握手扩展、证书、或者通过记录类型调制,加密后仍然能看到类型字段——这就是为什么基于记录类型的水印能穿透 TLS。
2.4 记录层处理的几个细节
有朋友问过我:“记录层会不会把一条握手消息拆到两个记录里?”会的。而且不同实现策略不一样。OpenSSL 喜欢在握手阶段每条记录只发一个消息,但 NSS 可能会攒几个小消息合并成一条记录。这种差异在水印检测时有影响:如果你在水印嵌入时假设消息边界就是记录边界,那就可能错位。
我个人习惯是,做水印方案时永远不依赖记录边界。我更推荐在消息内部做冗余标记,比如重复某个扩展、或者通过消息顺序来编码。这样记录层怎么分片都不影响。
2.5 记录层对水印的隐蔽性影响
记录层带来的最大好处——也是最大的挑战——是 加密。加密后,除了 Type、Version、Length 这三个明文字段,剩下的 fragment 全是密文。水印如果放在 fragment 里,对手要看懂水印就必须先解密。而解密的密钥只有通信双方掌握,所以对手无法提取水印。这是“前向安全”水印的基础。
但是,Type 和 Length 字段是明文的。我曾经参与过一个项目,想要通过“在正常应用数据记录中插入告警记录”来传递隐蔽信息。这个方案很隐蔽,因为告警记录也可以有任意数据(Alert 的 description 字段)。但问题是,网络监控设备可以观察到异常的告警频率,比如每分钟只发一个“0x15”的告警,这就暴露了水印的存在。
注意:记录层头部无法被加密保护。ContentType 和 Length 字段是第 5 类暴露信息,可用于流量指纹、水印调制。如果你的水印方案依赖这两个字段,务必做抗检测设计,比如伪装成正常业务的统计分布。
2.6 实操小结
这一节内容其实很基础,但基础的东西往往最容易被忽略。我把核心要点列出来,你动手时可以对照着检查:
- TLS 记录层封装格式:Type(1 字节)、Version(2 字节)、Length(2 字节)、Fragment。
- 记录层处理顺序:分段 → 压缩(可选) → MAC → 加密 → 加头。
- 水印可嵌入位置:Fragment(被加密)、Type(明文)、Length(明文)、Version(明文)。
- 记录分片与合并会导致水印检测的错位问题,推荐在消息内部做标记。