编程语言

invalid peer certificate: UnknownIssuer(Rust rustls)

作者: · 最后核实

报错信息invalid peer certificate: UnknownIssuer

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 里,它是连接错误最里层的原因:

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 解密导致的报错。
  • 修复: 改用系统证书或平台校验器,并把根证书装进系统;或者在代码里加入它。

2. 服务器没发中间证书

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

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 就要改用系统证书或单独加入私有根证书,用系统证书就把根证书装进系统。

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

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

reqwest 0.12,改用系统证书库而不是内置根证书(Cargo.toml):

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

reqwest 0.13,在保留平台根证书的同时加入私有根证书:

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,加载系统证书库:

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 主机和镜像:

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 证书包;证书链不完整就在服务器上修。

怎么确认已经修好

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

在和生产相同的镜像或系统上,请求应当成功,不再出现 UnknownIssuer。用 SSL 检测工具确认服务器证书链;想知道为什么缺一张中间证书浏览器没事、rustls 却失败,可以读证书链原理。

常见问题

为什么 curl 和浏览器信任这个网站,Rust 程序却不信任?

它们用的信任库不同。浏览器和 curl 用操作系统证书库或系统证书包;用 webpki-roots 编译的 Rust 程序,内置的是一份固定的 Mozilla 根证书,装在机器上的公司根证书对它不起作用。改用系统证书或平台校验器,或者显式加入根证书。

升级 webpki-roots 有用吗?

只有服务器的证书链指向一张新的公开根证书、而你的旧版 webpki-roots 里还没有时才有用。对私有 CA 和 HTTPS 解密代理永远没用,因为 webpki-roots 只包含 Mozilla 收录的公开根证书。

本机能跑,放进 scratch 镜像就报错,为什么?

在 Linux 上使用系统证书或平台校验器时,rustls 读取的是容器里的 CA 证书包。scratch 镜像里一张都没有,有些精简镜像也没装 ca-certificates 包。在构建阶段安装 ca-certificates,把 /etc/ssl/certs/ca-certificates.crt 复制进最终镜像,或者用 SSL_CERT_FILE 指向你随程序一起分发的证书包。

换成 native-tls 呢?

native-tls 在 Linux 上用 OpenSSL、在 Windows 上用 Schannel、在 macOS 上用 Security.framework,默认信任系统证书库,报错也会变成 OpenSSL 或平台的消息,而不是 UnknownIssuer。这是一个可行的选择,但“信任哪些根证书”的问题依然存在。

资料来源

  1. rustls docs: CertificateError
  2. rustls-platform-verifier README
  3. rustls-native-certs README
  4. reqwest CHANGELOG (v0.13.0 TLS changes)
  5. reqwest docs: ClientBuilder
  6. 清华大学开源软件镜像站 crates.io-index 帮助