# invalid peer certificate: UnknownIssuer（Rust rustls）

> rustls 无法把服务器证书链连到根证书库里的任何一张根证书。选对根证书来源（webpki-roots、系统证书或平台校验器），或者加入所需的根证书。

Source: https://ssl.sh/zh/errors/rust-invalid-peer-certificate-unknown-issuer/
Last updated: 2026-11-10

**`invalid peer certificate: UnknownIssuer` 的意思是：rustls 没法把服务器的证书链连到它手里根证书库中的任何一张根证书。** 浏览器能打开不代表 rustls 也能验证，差别在根证书库：Rust 程序信任的是编译进去的 Mozilla 列表或容器里的空证书包，而服务器证书链的终点是公司 CA、私有 CA，或者缺了中间证书。要修的是根证书来源。

## 这条报错是什么意思

这条报错表示证书链没有连到 rustls 信任的任何根证书。rustls 返回的是 `Error::InvalidCertificate(CertificateError::UnknownIssuer)`，显示为 `invalid peer certificate: UnknownIssuer`，Debug 输出里是 `InvalidCertificate(UnknownIssuer)`。rustls 文档对这个变体的说明是：证书链不是由已知的根证书签发的。在 reqwest、hyper、tonic 里，它是连接错误最里层的原因：

```text
error sending request for url (https://example.com/): ...: invalid peer certificate: UnknownIssuer
```

（reqwest 自己的消息只写 URL，要打印完整的 `source()` 链，比如用 anyhow 的 `{:#}`，才能看到 rustls 这一段。）

哪些根证书算“已知”，取决于依赖是怎么配置的：

- **webpki-roots：** 把 Mozilla 的公开根证书编译进二进制，只有更新这个 crate 才会变。
- **rustls-native-certs：** 运行时读取操作系统证书库；在 Unix 上支持 `SSL_CERT_FILE` 和 `SSL_CERT_DIR`。
- **rustls-platform-verifier：** 在 Windows、macOS、Android 上交给系统校验器；在 Linux 上用系统 CA 证书包加 webpki 校验。

reqwest 0.12 的 `rustls-tls` 特性选的是 webpki-roots；reqwest 0.13 把 rustls 改成默认后端，并默认使用 rustls-platform-verifier，所以升级 reqwest 会改变你信任的根证书。

排查时最关键的一步，是先弄清楚程序到底用了哪一种根证书来源。同一份代码，换一个 reqwest 版本或换一组特性，信任的根证书就完全不同：用 webpki-roots 时，往系统里装多少证书都没用；用系统证书时，容器里没有证书包就什么都不信任。所以先用下面的 `cargo tree` 命令确认依赖，再对照原因逐条检查。

## 常见原因（按出现频率排序）

最常见的原因是公司或私有 CA 不在 rustls 使用的根证书库里。

### 1. 公司或私有 CA 不在根证书库里

做 HTTPS 解密的代理和内网服务，证书由不在 Mozilla 列表里的根证书签发。用 webpki-roots 时它们永远不会被信任；用系统证书时则必须先装进系统。

- **确认：** `openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | openssl x509 -noout -issuer` 显示的是公司名称或安全厂商。见[企业 SSL 解密导致的报错](https://ssl.sh/zh/errors/corporate-ssl-inspection/)。
- **修复：** 改用系统证书或平台校验器，并把根证书装进系统；或者在代码里加入它。

### 2. 服务器没发中间证书

rustls 只用服务器发来的证书加上自己的根证书做校验，不会去下载缺失的中间证书。

- **确认：** `openssl s_client -showcerts` 只返回一张证书，或者[证书链补全工具](https://ssl.sh/zh/tools/chain-builder/)提示缺中间证书。
- **修复：** 在服务器上配置完整证书链（见[缺少中间证书](https://ssl.sh/zh/errors/missing-intermediate-certificate/)）。

### 3. 容器里没有 CA 证书包

`scratch` 镜像，以及没装 ca-certificates 包的精简镜像，都没有 `/etc/ssl/certs/ca-certificates.crt`。用系统证书时，rustls 没有任何可信任的根证书，访问哪个网站都失败。

- **确认：** 在容器里执行 `ls -l /etc/ssl/certs/`。
- **修复：** 在构建阶段安装 `ca-certificates`，再把证书包复制进最终镜像。

### 4. 开发环境的自签名证书

自签名的服务器证书自己就是签发者，除非你把它作为根证书加进去，否则就是“未知签发者”。更好的做法是建一个本地 CA 来签发开发证书，然后信任这个 CA。

### 5. webpki-roots 版本太旧

在你的 crate 版本发布之后才加入 Mozilla 根证书计划的公开根证书，旧版本不认识。`cargo update -p webpki-roots` 可以更新。

## 分平台修复命令

修法取决于根证书来源：用内置的 webpki-roots 就要改用系统证书或单独加入私有根证书，用系统证书就把根证书装进系统。

**先看你用的是哪种根证书来源：**

```bash
cargo tree -i webpki-roots
cargo tree -e features -i reqwest | head -20
```

**reqwest 0.12，改用系统证书库而不是内置根证书（Cargo.toml）：**

```toml
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-native-roots"] }
```

**reqwest 0.13，在保留平台根证书的同时加入私有根证书：**

```rust
let pem = std::fs::read("/etc/ssl/certs/company-root.pem")?;
let client = reqwest::Client::builder()
    .tls_certs_merge(reqwest::Certificate::from_pem_bundle(&pem)?)
    .build()?;
```

reqwest 0.12 上用 `.add_root_certificate(reqwest::Certificate::from_pem(&pem)?)`。

**直接使用 rustls，加载系统证书库：**

```rust
let mut roots = rustls::RootCertStore::empty();
roots.add_parsable_certificates(rustls_native_certs::load_native_certs().certs);
let config = rustls::ClientConfig::builder().with_root_certificates(roots).with_no_client_auth();
```

**Linux 主机和镜像：**

```dockerfile
FROM debian:bookworm-slim AS certs
RUN apt-get update && apt-get install -y ca-certificates
COPY company-root.crt /usr/local/share/ca-certificates/
RUN update-ca-certificates

FROM scratch
COPY your-binary /your-binary
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
```

**Windows 和 macOS：** 使用平台校验器（reqwest 0.13 的默认值）时，把根证书装进系统证书存储（域策略、`certlm.msc` 或钥匙串）就够了。

**国内环境：** crates.io 下载慢可以按清华镜像站的说明配置 `sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/`，但这只影响 cargo 下载依赖，不影响你的程序在运行时信任哪些根证书。在公司网络里开发时，如果访问任何 HTTPS 网站都报 UnknownIssuer，基本可以确定是上网行为管理在解密流量，按第 1 条处理。

## 不推荐：关闭证书校验

`danger_accept_invalid_certs(true)`（reqwest 0.13 改名为 `tls_danger_accept_invalid_certs`），或者一个永远返回成功的自定义 rustls `ServerCertVerifier`，都是靠“信任一切”让报错消失。reqwest 自己的文档就警告：这样会信任*任何*网站的*任何*证书，包括已过期的证书，只能作为最后手段。链路上的任何人都可以冒充你的 API，截获 token。

更安全的替代办法：用系统证书或平台校验器信任系统证书库；用 `tls_certs_merge` / `add_root_certificate` 只加入那一张私有根证书；容器里带上 CA 证书包；证书链不完整就在服务器上修。

## 怎么确认已经修好

```bash
./your-binary https://example.com/
SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt ./your-binary https://example.com/   # 使用系统证书时，测试指定证书包
```

在和生产相同的镜像或系统上，请求应当成功，不再出现 `UnknownIssuer`。用 [SSL 检测工具](https://ssl.sh/zh/tools/ssl-checker/)确认服务器证书链；想知道为什么缺一张中间证书浏览器没事、rustls 却失败，可以读[证书链原理](https://ssl.sh/zh/learn/certificate-chains/)。
