摘要

  • RFC 5280 的证书策略树会在多个策略映射指向同一状态时反复复制节点;很短的证书链也可能诱发指数级时间和内存开销。
  • RFC 9618 用带共享父节点的有向无环图表示同一可达关系,图的规模随输入中的策略和映射线性增长,同时保持路径有效性和最终有效策略集合不变。
  • 迁移不能只做功能回归。可信的上线凭证必须同时证明复杂策略场景的语义等价,以及对抗性输入下的 CPU、内存、队列和失败边界。

评审会上,旧版和新版验证器并排运行。普通证书全部通过,过期证书、错误签名和策略不兼容的证书全部拒绝。测试表里没有一处差异,团队准备签字。

那张表没有记录每个结论用了多少节点、多少内存和多少时间。后来,一条字节数并不大的客户端证书链占满了验证工作进程。它没有伪造签名,也没有取得身份;它只是让实现按照旧有表示法展开策略映射,直到其他请求排队。

RFC 9618揭示的正是这类盲点:结果相同,不代表安全属性相同。验证器不仅要给出正确答案,还要限制未获授权的输入为了得到这个答案能够命令它付出多少计算。

策略验证究竟在算什么

X.509 证书的 certificatePolicies 扩展可以列出一个或多个策略 OID,并可附带限定信息。CA 证书可以约束后续证书所接受的策略,也可以把签发方域中的策略 OID 映射为主体域中的另一个 OID。依赖方还可以提供自己愿意接受的初始策略集合。

RFC 5280在认证路径验证过程中计算哪些策略能够穿过整条证书链、映射和约束。这里不能混淆“构建路径”与“验证路径”。RFC 4158讨论如何寻找从目标证书到信任锚的候选链;RFC 9618 处理的是候选链已经存在之后,策略状态如何被求值。

RFC 5280 使用 valid_policy_tree 表示中间状态。树的每一条根到叶路径,代表一种策略映射的来路。这个表示法直观,却会重复保存相同事实:如果上一层两个签发方策略都映射到下一层同一个主体策略,树会建立两个相同节点;后续子节点又在每个副本下继续复制。

RFC 9618 给出一个最清楚的构造:每个中间证书包含两个策略 OID,并包含两者到下一层两者的完整笛卡尔积映射。每深入一层,策略树就翻倍。输入只增加少量证书和映射,内部对象却指数增长。

攻击者利用的是成本不对称。启用客户端证书验证的 TLS 服务、身份网关或其他 X.509 应用,可能在最终拒绝证书之前就耗尽 CPU 或内存。攻击目标不是取得合法结论,而是让结论迟迟无法产生。

这并非只存在于复杂度分析中。RFC 9618 明确引用 CVE-2023-0464 和 CVE-2023-23524。OpenSSL 的修复记录描述策略约束检查中的指数资源消耗及节点上限;Apple 安全公告说明处理恶意证书可能导致拒绝服务;OpenSSL 版本历史也保留了策略树节点限制的记录。

用共享节点保存关系,而不是枚举所有来路

RFC 9618 把状态替换为 valid_policy_graph。它是按证书深度分层的有向无环图:同一深度、同一策略 OID 最多只有一个节点;若多个上一层策略都能到达它,它就拥有多个父节点。下游状态只存一份。

图并没有丢掉旧树表达的事实。旧树等价于把图中每一条根到叶路径全部枚举出来。要判断最终有哪些策略可达,并不需要把所有重复路径实体化。因此,新算法能够保持认证路径的有效或无效结论,也保持最终有效策略集合。

改变的是运行上限。图的规模由证书链中策略和策略映射的总数线性约束。大输入仍然会带来成本,但很小的组合结构不再能命令验证器制造一个指数级更大的内部对象。

这也是“最小共同规范”的价值。标准固定 anyPolicy、计数器、映射、剪枝和最终集合的语义,却不要求所有产品使用相同内存布局。实现可以本地优化,但不能以改变答案为代价,也不能把无上限的工作藏在兼容性标签后面。

输出接口可能把风险重新造出来

RFC 5280 曾把完整 valid_policy_tree 描述为验证输出。即使新验证器内部使用图,旧调用方仍可能要求取得整棵树。此时,实现必须把共享节点沿每条路径复制,指数对象就在输出阶段复活。

RFC 9618 因此弃用完整策略树输出,并建议只返回受 CA 和用户约束后的策略集合。应用真正需要的通常是“哪些策略最终有效”,而不是每一条等价来路。

为了遗留兼容,系统仍可在调用方实际请求时延迟重建树。但延迟不等于有界,它只是把成本推到另一个时刻。这样的重建必须独立限流、隔离资源,并清楚记录由哪个业务所有者承担风险。

这里暴露的是控制权问题。如果一项无人负责的诊断 API 能迫使所有身份请求保留危险表示,它就事实上掌握了服务可用性。兼容性应由可识别的消费需求决定,而不是由历史对象的存在决定。

上限是缓解措施,不是语义证明

在图算法尚未覆盖全部系统时,限制证书链深度或策略树节点数可以减轻风险。OpenSSL 的公开记录就包含节点限制。但任何上限都会划出新的拒绝边界。

深度太低,会拒绝合法链;深度太高,仍可能容纳高分支映射。RFC 9618 指出,若增加每张证书的策略数量,即使深度受限,攻击仍可达到约 O(N^(深度/2)) 的增长。节点上限也只有在资源耗尽之前稳定触发,才真正保护服务。

因此证据不能止于一个配置数字。它应记录:在哪个节点停止、累计内存和 CPU、返回什么错误、调用方是否重试、并发请求是否被拖慢。缺少这些事实,“已经设置限制”只是政策宣告。

关闭策略处理也不是普遍解法。如果相关策略扩展被标记为 critical,无法处理它们的实现必须把它们视为未知关键扩展并拒绝证书,而不能静默忽略。这一规则守住的是签名覆盖的关键性契约,不应与树到图的机制混为一篇文章。

两份凭证必须在同一个测试中相遇

第一份是语义凭证。语料要覆盖普通策略、多重映射、anyPolicy、显式策略要求、禁止策略映射、禁止 anyPolicy、剪枝和最终空集合。每个向量都比较通过或拒绝,以及用户约束后的最终策略。若实现只是粗暴拒绝复杂合法链,速度再快也不是等价替换。

第二份是复杂度凭证。分别增加链深、每张证书的策略数量和父节点复用程度,记录编码字节数、图节点与边、峰值内存、分配次数、CPU 时间、墙钟时间、拒绝原因和并发队列影响。这样才能判断成本是否跟随输入线性变化,还是迁移后又出现新的隐性放大。

两份凭证不能来自互不相干的试验。每个语义案例都应有资源轨迹,每个压力案例都应保留准确结论。否则团队可能用改变语义换来“更快”,也可能在保留指数风险的情况下宣布“完全兼容”。

最后还要证明运行版本。记录实际加载的库、构建选项、策略处理配置、遗留树 API 的调用和进程身份。RFC 发布使新机制可用;只有部署证据才能说明它正在处理生产请求。

RFC 9618 并未解决所有 X.509 风险。路径搜索、签名计算、撤销查询、ASN.1 解析与应用授权都有自己的成本。它精准切断的是策略映射中的一种放大机制。正因为边界明确,领导者没有理由接受模糊的合规声明。

来源