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。
问: 同一张自签证书,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:。