摘要

  • TLS 1.3 的 certificate_authorities 扩展承载一组按顺序排列、以 DER 编码的可分辨名称。它帮助对端选择证书链,却不携带 CA 公钥、信任锚配置、完整验证路径或访问许可。
  • 同一扩展既可由客户端放进 ClientHello,影响服务器证书选择,也可由服务器放进 CertificateRequest,影响客户端证书选择。方向不同,信息所有者、隐私代价与失败后果也不同。
  • 可审计的相互 TLS 至少要留下四次独立判定:谁生成了名称清单、为什么选择这张凭据、验证器最终接受哪条路径、应用又把已认证身份映射成什么权限。

名称命中了,主体验证仍可被业务拒绝

假设一个工作负载持有两张客户端证书。服务器在 CertificateRequest 中列出一个 CA 的可分辨名称,本地 TLS 库因此选择第一张证书,发送证书链,并完成私钥持有证明。服务器从实际验证库中找到锚点,路径检查通过。

此时应用仍可能合理地拒绝请求。证书的 URI SAN 可能属于另一个命名空间,主体可能未登记到该租户,策略要求的用途或角色也可能不存在。密码学回答的是“这把私钥和这条合格证书路径是否对应”;业务授权回答的是“这个已认证主体现在能否执行这项动作”。

因此,ca_match=true 不是简化后的安全结论,而是丢失了判定层级的数据。它无法说明名称来自哪个握手消息,哪张证书因何被选中,锚点究竟是哪一把密钥,更无法说明应用准入是否成功。

可分辨名称不是证书,也不是信任锚

IANA 为 certificate_authorities 分配扩展值 47。RFC 9846 规定,其内容是一项或多项 DER 编码的 X.501 可分辨名称;名称可指向希望对端采用的信任锚或从属 CA,并应当用于指导凭据选择。

这个结构只传名称,不传相应证书、公开密钥、基本约束、名称约束或证书策略。同名 CA 证书可以使用不同密钥;能从文件中提取 subject 名称,也不意味着文件中的对象具有 CA 能力。名称匹配无法证明发送方信任哪一把密钥。

信任锚通常通过独立渠道配置,因此也可能根本不出现在传输证书链中。真正的路径验证还要检查签名、有效期、基本约束、名称约束、密钥用途、策略输入和具体信任锚。清单成员资格不能替代其中任何一步。

对清单存在性最稳妥的表述只有一句:发送方在这个消息上下文里公开了这个编码名称。出现不代表该名称之下的每张证书都能通过;缺席也不代表普遍不信任。清单可能为隐私而缩减,可能只服务当前凭据选择,也可能与另一套验证状态处于不同更新代际。

两个方向不能合并成一种策略

客户端把该扩展放入 ClientHello,是在告诉服务器哪些 CA 名称可影响服务器证书及备用链的选择。服务器把它放入 CertificateRequest,则是在提示客户端应该从自己的凭据中挑哪一张。

前者还带来直接的隐私问题。OpenSSL 文档明确提示,客户端 CA 名称会以明文送到服务器;把企业信任清单原样塞进每次握手,可能泄露内部机构名称或 PKI 结构。后者虽能减少客户端选择歧义,却可能因清单过长增加握手字节、解析负担和兼容风险。

TLS 1.3 不使用旧的 trusted_ca_keys 扩展。兼容旧版本的 ClientHello 仍可能携带遗留信号,所以遥测必须同时记录协商版本、消息方向和扩展类型,不能把“出现过 CA 相关字段”写成一项笼统能力。

选择是一组约束的交集

CA 名称只是选择条件之一。TLS 库还要处理 signature_algorithms,有时还包括 signature_algorithms_cert;随后再结合密钥类型、私钥是否可用、证书签名算法、有效期、Key Usage、EKU、服务器名、OID 过滤条件以及本地能构造出的备用证书链。

这意味着未被选择的候选项同样重要。某张证书可能因 CA 名称不符而被剔除,也可能名称匹配但算法不符、EKU 错误、私钥缺失或无法建立合适链。只保存最后一张 leaf 指纹,会抹掉真正控制选择结果的规则。

当客户端没有合适证书时,TLS 1.3 给出了明确且诚实的结果:发送空 Certificate。服务器可依据应用约定继续,或以 certificate_required 拒绝。系统不应临时拼接无关证书链,更不应把“没有凭据”悄悄改写成匿名成功。

名称清单与验证库是两套运行状态

OpenSSL 的接口划分把边界说得很清楚:CA 名称清单用于向对端发送名称;设置该清单不会让这些 CA 自动受信任。验证器必须通过独立的验证位置或信任库接口加载证书与锚点。

可以据此设计一个关键负向测试:服务器公布某个 CA subject 名称,却不在实际验证库中信任对应密钥。客户端仍可能挑出匹配链,但服务器最终返回 unknown_ca。反过来,验证库可能接受某条链,而该 CA 名称并未出现在提示清单中。

这不是库行为前后矛盾,而是清单生成与路径验证本来就有各自的配置代际。热更新可能只改一边。生产证据需要分别记录两者的哈希、激活时间和适用进程,再把具体连接绑定到实际执行的版本。

OpenSSL 的客户端 CA 名称加载函数也暴露出另一个陷阱:它提取 subject 名称,输入并不限于 CA 证书。因此“文件加载成功”只能证明解析成功,不能证明每个名称都对应一个有 CA 能力且受信任的对象。

路径成功之后才轮到身份准入

收到证书链后,RFC 5280 路径处理与相应 TLS 约定决定它能否到达可接受锚点,并满足用途和约束。端点身份规则再解释 SAN、名称或其他标识。即使这些步骤全部成功,应用权限仍未自动产生。

服务还要把身份映射到租户、账号、工作负载、角色与动作。这项映射可能依赖精确 SAN 形式、签发者与主体组合、证书策略 OID、登记状态或外部授权数据。一张完全有效的证书被业务拒绝,可能正是最正确的行为。

客户端也不能只凭握手完成就推断服务器已认可其相互认证身份。应用协议或部署约定可能需要返回明确结果。若把“TLS 完成”直接当作“角色授予”,跨层权限泄漏就已经发生。

回调已安装,不代表该路径实际运行

OpenSSL、GnuTLS 与 BoringSSL 都提供证书选择、CA 名称读取和验证相关回调。配置了回调只证明代码具备这项能力;只有带连接标识的真实调用记录,才能证明某次握手走过这条路径。

BoringSSL 对部分 CA 清单读取值还限定了生命周期:只应在选择回调或暂停握手的有效期内使用。需要长期证据时,应当在有效作用域内复制或哈希 DER 字节,而不是保存悬空引用。GnuTLS 也把证书选择回调与信任列表、会话验证函数分开;“被选中”和“已验证”在接口层同样是两件事。

PSK 恢复会改变证据边界。在恢复的 TLS 1.3 主握手中,依赖 PSK 认证的服务器不会重新发起一次完整 CertificateRequest 交换。若回调没有运行,指标就不能声称重新处理了 CA 清单或重新验证了客户端证书。

用负向测试守住提示权

公布一个名称,却不信任同名密钥;准备两张同 subject、不同 key 的 CA,只信任其中一张;提供名称匹配但 EKU 错误、已过期或策略不符的 leaf;让私钥不可用,并确认系统留下明确淘汰原因。

让证书链验证通过,再在租户或角色边界拒绝主体。移除所有合适客户端凭据,观察空 Certificate 与服务器后续决策。分别只更新验证库、只更新名称清单,检查告警能否指出究竟是哪一个代际发生漂移。

抓取客户端清单,衡量组织信息泄露;测试大量或超长名称;恢复会话并确认“新客户端认证”计数不增长;验证遥测能区分回调已安装、实际调用、凭据已选择、路径已验证和动作已授权。

最终需要保存的是一条分层证据链:公布名称、候选集合、选择凭据、私钥持有证明、验证路径、解释身份、授权动作。每一级都有自己的责任人,任何下一级都不能借用上一级尚未作出的权力。

来源