服务器证书的用途看起来没问题,密钥匹配,有效期也正常,连接却仍然以 unsupported certificate purpose 失败。真正能解决它的细节,通常就在报错的那一行里:一个深度值,以及一个指向链中更高层证书的主题名。这里其实在跑两个不同的检验,而只有其中一个与叶子证书有关。把两者对比起来,就能解释为什么换掉服务器证书毫无效果。

验证器到底拒绝了哪张证书

一条链并不是同一个对象重复三次。叶子携带服务器名称与密钥,中间证书把它连接到根,而每一张都有自己的有效期、约束和用途。当报错给出深度值时,那个数字标出的是验证器所构建的链中的一个位置;如果服务器发来一条意料之外的链,这个位置未必是你以为的那个。

要读的是那个深度上证书的字段,而不是你假定有错的那张证书。只请求那两个约束签发的扩展,别把所有字段都倒出来,因为答案几乎总是其中之一。扩展的输出可以放心分享;私钥则永远不行。

openssl x509 -in issuing-ca.pem -noout -subject -issuer -ext basicConstraints -ext extendedKeyUsage

如果被点名的证书是一张中间证书,而它的扩展密钥用途提到了与 TLS 服务器认证无关的东西,剩下的失败就顺理成章了。这条链无法用于 OpenVPN 所请求的用途。

叶子用途与链策略是两个检验

叶子检验问的是一个很窄的问题:这张终端实体证书是否允许为这个名字做服务器认证?链检验问的更宽:路径上的每一张证书是否都可以服务这个用途?一张只被允许签发客户端证书的证书颁发机构,会通过它自己的签名检查,却仍然在第二个检验上失败,因为它所授权的路径不包含应用所需的用途。两个检验以同样的字眼失败,修法却完全不同。

这就是为什么一张刚刚签发、每个可见字段都正确的叶子证书可以立刻失败。这也是为什么修法通常落在签发机构的配置文件上,而不是落在 VPN 配置上。

EKU 缺失不等于 EKU 受限

RFC 5280 让扩展密钥用途扩展成为可选项,而一旦出现就具有约束力。因此,一张没有该扩展的 CA 证书并没有对服务器认证说“不”,而一张只列了无关用途的证书则说得很具体。把这两种状态当作等价,会得出错误的诊断,更糟的是,会得出错误的修法。这个区别也解释了为什么两张在控制台里看起来相似的机构,在链里的表现完全不同。

库的行为在这里也不统一。OpenSSL 的验证选项文档 描述了它对 CA 证书用途检查的处理方式,而这种处理是一种实现策略,并不是每个 TLS 栈都完全照做的规则。描述你的验证器实际做了什么,再用你真正运行的版本来确认。

重新签发前,先离线复现失败

离线验证把应用从问题里摘出去。如果同一条链在同一个用途下以同样的方式失败,那么问题在证书,服务器配置再怎么改也藏不住。如果链在离线时通过,服务仍然拒绝,那么应用请求的是另一个用途,或者它用的是一条与你测试时不同的链。

openssl verify -purpose sslserver -show_chain -CAfile root-ca.pem -untrusted issuing-ca.pem vpn-server.pem

-show_chain 很重要,因为它会打印工具实际构建出的路径,而那往往不是你提供的文件顺序。那份输出是发现以下情况最快的途径:第二张中间证书正从信任库中被拉进来;服务器发来了你并不想要的交叉签名变体;或者你从教程里抄来的用途标志对应的是另一个角色,而不是正在提供服务的这个角色。

改签发策略,而不是改验证调用

持久的修法,是按一份允许它必须服务的用途的策略来签发中间证书,并把这条约束写下来、纳入评审。给每个机构加上每一种用途并不是策略;它抹掉了当初让这个扩展有意义的约束,而且往往比引发它的那次事故活得更久。在配置文件里写一句短注,说明该机构可以认证哪些角色,能避免下一次重签时再吵一遍。

关掉验证是另一个诱人的捷径,它把证书问题变成了信任问题。把用途检查留在原处,改正链,再跑一次同样的离线验证,确认这条路径现在服务的是预期的角色,然后再去动服务。

相关文章