Rust in Everything
基于 rustls 0.23.46

invalid peer certificate: UnknownIssuer

主线位置:校验主线的路径构建一步。rustls-webpki 从叶子往上找签发者,先找信任锚,再找服务器发来的中间证书;两处都找不到就是 UnknownIssuer。路径构建的完整过程见路径构建,RootCertStore 存的是什么见服务器证书。

flowchart TB
  leaf["叶子的 issuer"] --> ta{"有锚的 subject 相同"}
  ta -->|有| sig["验签名"]
  ta -->|没有| im{"有中间证书 subject 相同"}
  im -->|有| up["对它递归"]
  im -->|没有| err["UnknownIssuer"]
  up --> ta
  err --> alert["告警 unknown_ca"]
  sig --> ok["路径成立"]

实验 crate:exp-rustls/pita-lab,src/bin/p03_issuer.rs(内存握手)、src/bin/p03_server.rs 加 run-p03-openssl.sh(127.0.0.1 上给 openssl s_client 看)。证书全部由 rcgen 现场生成。M2 Max、macOS 27、rustc 1.99.0,release,ring provider,TLS 1.3。

现象

PKI 是「根 → 中间 → 叶子(localhost)」,客户端信任根。三种常见的错法:

== A: server sends only the leaf (cert.pem instead of fullchain.pem)
  (server then sees: received fatal alert: UnknownCA)
leaf only: client error: invalid peer certificate: UnknownIssuer
    debug: InvalidCertificate(UnknownIssuer)

== B: self-signed server certificate, client uses webpki-roots
webpki-roots anchors: 118
  (server then sees: received fatal alert: UnknownCA)
self-signed vs webpki-roots: client error: invalid peer certificate: UnknownIssuer
    debug: InvalidCertificate(UnknownIssuer)

== C: empty root store
  (server then sees: received fatal alert: UnknownCA)
empty RootCertStore: client error: invalid peer certificate: UnknownIssuer
    debug: InvalidCertificate(UnknownIssuer)
  • A 最常见:服务端配置的证书文件里只有叶子一个 PEM 块,中间证书在另一个文件里没配上。
  • B 是开发环境里的自签证书,客户端用的是 webpki-roots(Mozilla 的 118 个根)。
  • C 是根证书库根本没填:忘了加载、文件路径错了、add_parsable_certificates 全部解析失败而没检查返回值。
  • 三种情况的报错完全一样,错误里没有任何关于「缺哪张」的信息。服务端那边只收到一个 unknown_ca 告警。

原因

rustls 把叶子、中间证书和根证书库交给 rustls-webpki,路径构建在 build_chain_inner。每一层先在信任锚里找,再在服务器发来的中间证书里找,比的都是「候选的 subject 等于当前证书的 issuer」:

        let result =
            loop_while_non_fatal_error(Error::UnknownIssuer, self.trust_anchors, |trust_anchor| {
                let trust_anchor_subject = untrusted::Input::from(trust_anchor.subject.as_ref());
                if !public_values_eq(path.head().issuer, trust_anchor_subject) {
                    return Err(Error::UnknownIssuer.into());
                }
                // ...
        loop_while_non_fatal_error(err, self.intermediate_certs, |cert_der| {
            let potential_issuer = Cert::from_der(untrusted::Input::from(cert_der))?;
            if !public_values_eq(potential_issuer.subject, path.head().issuer) {
                return Err(Error::UnknownIssuer.into());
            }

UnknownIssuer 是两个循环的默认错误,候选列表为空或者没有一个名字对得上,返回的就是它。对照三种情况:

  • A:叶子的 issuer 是「Pita Intermediate CA」。锚里只有「Pita Root CA」,中间证书列表为空,两边都找不到。客户端不会自己去补中间证书:rustls-webpki 只用握手里收到的那几张。
  • B:自签证书的 issuer 就是它自己的 subject,118 个锚里没有这个名字。rustls-webpki 没有「自签」这个概念,不会单独报出来。
  • C:锚列表为空,第一个循环一次都不进,直接带着默认错误进入第二个循环,最后同样是 UnknownIssuer。用 ClientConfig::with_root_certificates 传空库不会在构建时报错;WebPkiServerVerifier::builder(..).build() 会返回 NoRootAnchors(见服务器证书)。

rustls 再经 pki_error 把它翻译成 CertificateError::UnknownIssuer,客户端随后发 unknown_ca 告警,服务端看到的就是 received fatal alert: UnknownCA。

📝NOTE
没有蠢问题

问: 同一张自签证书,openssl s_client 会明确说「self-signed certificate」,rustls 为什么只说 UnknownIssuer?

答: OpenSSL 有专门的错误码,rustls-webpki 没有。run-p03-openssl.sh 的 self 模式让 p03_server 用 rcgen 的自签证书,openssl s_client(Homebrew 的 OpenSSL 4.1.0)不给 -CAfile:

verify error:num=18:self-signed certificate
 0 s:CN=rcgen self signed cert
   i:CN=rcgen self signed cert
Verify return code: 18 (self-signed certificate)

rustls-webpki 只做一件事:拿 issuer 去找 subject 相同的锚或中间证书。自签证书的 issuer 等于自己的 subject,只要它不在锚里,结果就和「找不到签发者」没有区别。想区分,自己比一下证书的 issuer 和 subject:s: 和 i: 两行相同就是自签。

修法

A:服务端发完整链。 链是叶子在前、中间证书在后(根可以不发):

A fixed: full chain: Ok
full chain + root sent too: Ok
  (server then sees: received fatal alert: UnknownCA)
leaf + root, intermediate missing: client error: invalid peer certificate: UnknownIssuer
    debug: InvalidCertificate(UnknownIssuer)
leaf + unrelated cert + intermediate: Ok
  • 多发一张根证书无害,客户端在锚里已经有它。
  • 发了根却漏了中间证书仍然失败:根不是叶子的直接签发者。
  • 中间证书之间混进一张无关的证书也没关系,rustls-webpki 按名字挑,不按顺序。
  • 只有第一张有位置要求:with_single_cert 经 CertifiedKey::from_der 检查私钥和链上第一张证书的公钥是否一致,把中间证书放在最前面会在启动时报 InconsistentKeys(KeyMismatch),而不是等到握手:
with_single_cert([intermediate, leaf], leaf key): Some(InconsistentKeys(KeyMismatch))

证书文件里要有叶子和全部中间证书,有几个 CERTIFICATE 块就读出几张,怎么读见 PEM 的读法。

B 和 C:客户端信任签发者。 自签证书就把这张证书本身加进 RootCertStore;内部 CA 签的就加 CA 的根:

B fixed: self-signed added as anchor: Ok

锚不必是自签的根,加一张中间证书也能用。在 webpki-roots 之外再加内部 CA 的做法,以及平台校验器,见只用 webpki-roots 时的内网 CA。不要为了绕过它换一个什么都不查的校验器:那会连 CertificateVerify 签名一起跳过,见危险的校验器。

怎么检查

看服务器到底发了几张。 用本机 OpenSSL 连过去,-showcerts 打印服务器发来的每一张证书的 subject 和 issuer。p03_server 分别只发叶子和发完整链:

### mode leaf
verify error:num=20:unable to get local issuer certificate
verify error:num=21:unable to verify the first certificate
 0 s:CN=leaf
   i:CN=Pita Intermediate CA
Verify return code: 21 (unable to verify the first certificate)
### mode full
 0 s:CN=leaf
   i:CN=Pita Intermediate CA
 1 s:CN=Pita Intermediate CA
   i:CN=Pita Root CA
Verify return code: 0 (ok)

两次都给了 -CAfile 指向根证书。只有 0 s: 一张、它的 i: 又不是你信任的根,就是情况 A。链完整时,第 n 张的 i: 等于第 n+1 张的 s:,最后一张的 i: 应该是客户端信任的某个根。

看客户端信任了什么。 构建完 RootCertStore 打一行 roots.len();add_parsable_certificates 返回 (成功数, 失败数),失败数不为 0 要查。

小结

  • 漏发中间证书、自签证书、根证书库为空,报的都是 invalid peer certificate: UnknownIssuer,服务端只看到 UnknownCA 告警。
  • rustls-webpki 只用握手里收到的中间证书和本地的锚,按 issuer 对 subject 匹配;没有「自签」这个单独的错误。
  • 服务端发完整链:叶子必须是第一张,其余顺序无所谓,多发根无害。
  • 客户端把自签证书或内部 CA 加进 RootCertStore,不要换成不校验的校验器。
  • openssl s_client -showcerts 看对方发了哪几张,逐张对 s: 和 i:。

💬 关联讨论

发起讨论
加载中...