TLS加密通信流程与数据包结构解析

这一章咱们聊聊TLS握手的细节。说实话,很多做水印嵌入的同行,一听到TLS就头大——觉得那是网络层的黑盒。但我个人习惯是,不管做什么安全相关工作,先把这个握手流程摸透。你想想看,水印数据要在TLS隧道里藏,你不了解数据包长什么样,怎么知道在哪挖洞呢?

先甩一张整体流程图,后面的讲解都绕不开它。

TLS 1.3 握手核心流程(简化) 客户端 (Client) 服务端 (Server) ① ClientHello ② ServerHello + 证书 + KeyShare 客户端计算共享密钥 ③ Finished (客户端) 服务端计算共享密钥 ④ Finished (服务端) 安全通道建立 安全通道建立 ⑤ 应用数据加密传输(水印嵌入点在此阶段) 注意:握手中的每个消息都封装在 TLS Record 中,Record Header 5字节

3.1 握手协议:不只是打个招呼

很多人以为TLS握手就是“你好我好大家好”,交换个密钥就完事。其实没那么简单。我做过一个项目,要往HTTPS视频流里嵌水印,一开始以为只要拿到加密后的payload就行,结果发现握手阶段的数据包结构直接影响水印位置的选取——错过握手,你就不知道密钥是什么。

TLS握手主要解决三个问题:

  • 身份验证:服务端(有时也验证客户端)出示证书,证明自己是合法的。
  • 密钥协商:双方用非对称加密交换临时密钥,然后各自计算出共享密钥。
  • 参数确认:确定要用的加密套件、协议版本、扩展字段等。

在TLS 1.3里,握手效率高了很多,以前要两次往返的,现在一次往返基本搞定。嗯,这里要注意:1-RTT模式是主流,但还保留了0-RTT(早发数据)模式——那个对水印嵌入来说坑比较多,后面章节会单说。

核心认知:TLS握手产生的共享密钥,是所有加密通信的根。水印要是想在应用层做,必须知道这个密钥或者计算路径。否则你就只能干瞪眼看着密文流。”

3.2 数据包结构:TLS Record 解剖

TLS不直接把原始数据扔到TCP流里。它每一层都套了格式。我当初第一次用Wireshark抓TLS包时,看到那一堆十六进制吓了一跳——其实就是Record Header + 负载。

一个标准的TLS Record长这样:

+-------+-------+-------+-------+-------+------...------+
| 类型  | 版本  | 长度(2B) |     负载(加密或无)       |
| (1B)  | (2B)  |         |                             |
+-------+-------+-------+-------+------...------------+

字段说明:

字段长度说明
Content Type1字节标识记录类型:20=ChangeCipherSpec, 21=Alert, 22=Handshake, 23=Application Data
Protocol Version2字节如 0x0303 表示 TLS 1.2,0x0304 表示 TLS 1.3
Length2字节负载(Payload)长度,不包括头部
Payload可变握手消息或加密的应用数据

讲个坑:我刚入行时用TLS 1.3抓包,发现版本字段显示的是0x0303(TLS 1.2)——别慌,这是为了兼容性做的伪装。真正的版本在扩展字段里。我当时查了半天文档才明白。所以,解码TLS包时不要只看版本号。

3.3 加密后的应用数据:水印的潜在战场

握手完成后,双方开始发送 Content Type = 23 的记录。这些记录的Payload是被加密的。加密使用对称密钥(由握手阶段派生),常见算法有AES-GCM、ChaCha20-Poly1305等。

每个应用数据记录的结构(以TLS 1.3为例):

+--------+--------+--------+--------+--------+--------+--------+
|  nonce  | 加密后的数据  | 认证标签  |   (可选:记录号?)   |
| (12B)  |  (可变)       | (16B)   |                       |
+--------+--------+--------+--------+--------+--------+--------+

这里有个关键点:加密是对整个Payload做的,包括HTTP头部和body。所以你能看到的只有一串密密麻麻的二进制。要往里面嵌水印,你不能直接修改密文——改了认证标签就过不去,接收端会报警。

警告:不要尝试修改TLS加密后的数据包内容。哪怕只改1比特,认证标签校验就会失败,连接被强制断开。我曾经见过有团队想“劫持”TLS流做盲水印,结果直接把服务搞崩了。正确的做法是在加密前或解密后操作。

3.4 关键扩展字段:水印嵌入的信息载体

TLS握手消息里有很多扩展字段(Extensions)。比如 supported_versions、key_share、server_name 等。这些东西在握手阶段是明文的!

我有个思路:可以在合法的扩展字段里“夹带”一些自定义数据,当作水印的辅助信息(比如嵌入参数、密钥ID)。自定义扩展需要IANA注册,但在实验或内网环境,你可以用 0xFF00-0xFFFF 范围的私有扩展号来做试验。

扩展名称类型值是否明文说明
Server Name (SNI)0x0000明文用于虚拟主机,水印可以编入域名子串?但风险大。
Supported Groups0x000a明文支持哪些椭圆曲线,可做隐写。
Key Share0x0033明文(公钥部分)包含椭圆曲线公钥,其字节可做微调嵌入水印。
Custom/Private0xFF00-0xFFFF明文私有扩展,可以携带任意字节。

但是! 修改这些扩展字段会影响握手兼容性。我曾经试过在Key Share公钥里塞几个比特,结果有些客户端直接拒绝握手——因为非法的公钥值会导致密钥派生失败。稳妥的做法是:只在自定义扩展字段里加信息,而且只在支持该扩展的双方之间用。

个人经验:我在一个内部工具里,用TLS自定义扩展做过水印密钥的带外传输。扩展里放一个短的JSON,包含水印算法版本、嵌入强度、密钥种子。服务端收到后解析,然后用这些参数去解码应用层的水印。注意扩展总长度不要超过2^16-1,而且要在ClientHello和ServerHello里对称传递。

3.5 代理与中间人场景下的数据包变化

现实网络里,TLS流量经常经过代理、CDN、负载均衡器。这些中间件可能解密再加密(TLS Termination),也可能直接转发(TLS Passthrough)。

对于水印嵌入来说:

  • Passthrough:中间设备不碰数据,你只能在端点操作。
  • Termination:代理拥有证书,能看到明文。这时水印可以在代理端嵌入,再加密转发。但注意代理再加密时使用的密钥可能与原始服务端不同。

我踩过一个坑:公司内部用反向代理做SSL卸载,我写的嵌入模块集成在应用服务器上,忘了代理那头又包了一层TLS。结果水印被代理加密层吃掉了,白忙活。后来改成在代理端嵌入才解决。

// 示例:Go语言中获取TLS连接中的握手信息(仅示意)
conn := tls.Conn
state := conn.ConnectionState()
// state.NegotiatedProtocol 协商的ALPN
// state.ServerCertificate 服务端证书
// state.OCSPResponse OCSP装订
// 注意:这些信息只能在握手完成后获取,不能修改已建立的连接

3.6 总结一下本章重点

  • TLS握手分成四个消息段(1-RTT),产生共享密钥。
  • 数据包结构为Record + Payload,加密后不可篡改。
  • 扩展字段可在握手阶段携带自定义信息,但需谨慎兼容性。
  • 代理场景下,水印嵌入点要选对——要么两端都做,要么在TLS终结节点做。