摘要

  • 只有当接收方先声明自己能够解压哪些算法时,RFC 8879 才允许 TLS 1.3 发送方用 CompressedCertificate 代替普通 Certificate 消息。算法声明只是接收能力,不证明压缩真正发生,也不等于给予对端无限资源。
  • uncompressed_length 必须与最终重建字节数完全一致,解压输出不得越过该值;实现还可以采用远低于 TLS 24 位上限的本地限制。这些判断发生在证书能够认证对端之前。
  • 解压完成不等于验证成功。证据链应分别记录算法声明、实际选择、压缩字节、声明与真实长度、重建消息哈希、解析出的证书链、验证结论、告警、非压缩回退及运输结果。

信任尚未建立,资源边界已经生效

设想一台入口设备同时处理数千次新握手。客户端在 ClientHello 中表示能接收 zlib、Brotli 与 Zstandard 压缩的服务器证书。服务器随后发来很小的 CompressedCertificate,选择 Brotli,却把 uncompressed_length 写成十二兆字节。

真正可能证明服务器身份的 Certificate 消息仍被包在压缩内容里。客户端不能先拿证书的 CA、域名或公钥来决定是否接受这次资源请求。它必须先检查算法是否在自己的清单中,再用普通 Certificate 路径的相同大小限制审查声明值,约束解压器绝不越界,并要求实际输出长度一字节不差。

这就是压缩功能背后的权限结构。发送方可以在接收方给出的算法集合中挑选一种表示;接收方仍决定愿意运行哪些解码器,以及一个尚未认证的连接最多消耗多少内存和 CPU。声明支持 Brotli,不是把内存调度权交给服务器。

TLS 的三字节长度字段可以表示最高 16,777,215 字节,但 RFC 8879 明确允许实现采用更低上限。压缩与未压缩路径必须服从同一限制。如果一份普通 Certificate 因过大而应被拒绝,就不能因为它在线上看起来很小而绕过规则。

声明、选择和成功不能合并计数

compress_certificate 是单向信号。客户端在 ClientHello 中列出自己可用于解压服务器 Certificate 的算法;服务器只有在 CertificateRequest 中列出算法,才表示愿意解压客户端 Certificate。协议没有对应的响应扩展。

发送方可以从收到的列表中选择一种,也可以继续发送普通 Certificate。因此,抓包里出现扩展 27,只能证明接收能力声明,不能证明后续出现了握手类型 25。把“客户端声明支持”当作“压缩握手成功”,是把可能性、执行与结果混为一个指标。

方向也不能推断。客户端能接收压缩的服务器证书,不代表它能发送压缩的客户端证书。库的编译选项与应用配置还可能分别关闭发送或接收。一次成功只能说明该连接、该方向、该算法和该实现路径,而不是系统的对称能力。

该扩展只适用于 TLS 1.3 及以后版本;协商到 TLS 1.2 时应被忽略。它也不是旧式 TLS record compression。后者压缩一般记录并与 CRIME 风险相关;RFC 8879 只改变一个握手消息的表示,重建之后仍回到普通证书处理。

压缩对象是完整编码消息

算法输入不是磁盘上的 PEM 文件,也不是几张独立 X.509 证书。它是原本会发送的完整 TLS Certificate 消息,包括请求上下文、证书列表以及每个 CertificateEntry 的扩展。如果其他扩展改变了消息内容,被压缩的也是改变后的精确字节。

所以审计至少需要两层哈希。第一层标识收到的算法、头部与压缩 payload;第二层标识解压重建后的完整 Certificate 消息。之后再记录叶证书与中间证书指纹,说明普通验证真正检查了哪些对象。

不同算法可以把同一输入变成完全不同的线上字节;同一算法的库版本升级,也可能改变压缩表示而不改变重建结果。只有压缩 payload 哈希,无法跨算法证明逻辑证书消息一致,更无法证明信任链通过。

预压缩缓存同样受这个边界约束。OpenSSL 允许服务器预先压缩已配置证书,而客户端 Certificate 因包含服务器提供的握手专用 context,必须按请求即时生成。服务器缓存也不能只绑定域名:链条续期、中间证书顺序或每项扩展变化,都可能让旧压缩对象不再对应当前消息。

长度字段是门槛,不是成绩

CompressedCertificate 包含算法 ID、24 位的 uncompressed_length 与压缩内容。解码失败、输出超过声明值或最终长度不一致时,接收方必须用 bad_certificate 终止连接。限制应在输出过程中执行,而不是等到内存已经写满才核对。

声明长度有助于提前拒绝和规划 buffer,却不证明对端诚实,也不保证重建内容可安全解析。一个没有越过协议上限的数字,在高并发服务里仍可能过大。本地上限应来自并发、内存与故障实验,而不是简单复制协议字段的最大值。

“压缩后一定更小”也不是有效性条件。BoringSSL 测试特意包含缩小、膨胀与随机转换。协议要求的是在选定算法与边界内精确恢复输入;压缩率属于性能结果。即使输出变大,只要格式与限制正确,也属于该测试要覆盖的路径。

因此应分别观察声明长度、真实长度、峰值分配、解压耗时和错误类型。优秀的比率可能在大并发下造成内存尖峰;并不惊艳的比率,也可能恰好少发一个 packet。单独一个百分比无法证明安全或延迟收益。

解压结束,验证才真正开始

重建成功后,Certificate 必须像未压缩到达时一样接受处理:解析消息、构建证书链、检查名称与时间、执行本地策略,再验证 TLS 1.3 transcript 中的 CertificateVerify。压缩不会修复过期证书、错误域名、未知 CA 或签名失败。

失败原因应保持分层。未声明的算法属于协商错误;越界、解码错误或长度不符属于重建错误;重建消息结构非法属于解析错误;链或名称不成立属于验证错误;CertificateVerify 不正确属于握手认证错误。

把这些都记为“TLS 证书错误”,会丢掉真正采取拒绝动作的控制点。反过来,把 decompression_success 当成认证成功也同样错误:它没有证明预期证书存在,没有证明私钥持有,更没有授予应用交易权限。

运行代码能显示这些界线。BoringSSL 先构造普通 Certificate,记录其精确长度,再调用选定的压缩回调并封装新消息;测试则覆盖错误长度和异常转换。OpenSSL 分别暴露发送、接收、算法偏好与服务器预压缩控制。注册表、库代码和生产应用配置必须逐项取证。

性能结论必须接到完整结果

第一次握手中,证书链往往占用大部分字节。压缩可能减少 records、packets,甚至避免一次运输往返,但结果取决于链条、拥塞窗口、丢包、CPU、缓存和双方共同算法。磁盘 PEM 大小不能代替线上编码消息大小。

测量应同时记录方向与算法声明、实际消息、压缩与原始长度、包与重传、CPU、内存、验证结论和握手完成时间,并使用相同证书身份、相同验证政策的未压缩连接作为对照。

没有共同算法时,发送普通 Certificate 是协议允许的回退。回退率上升可能表示客户端构成变化,也可能是某个 build 丢失了解码器或 TLS 终止层移动。只要两条路径共享限制和验证政策,回退就是表示变化,而不是信任降级。

来源