Rust in Everything
基于 reqwest 0.13.5

danger_accept_invalid_certs 连冒名者也接受

主线位置:连接主线的 TLS 一步,build 时按配置四选一装好证书校验器。四种校验器各信任谁、NoVerifier 和 IgnoreHostname 的代码,见 TLS 配置 的「证书校验器:四选一」;no-op 校验器在 rustls 层面的后果见 rustls 卷的 跳过证书校验,中间人照样握手成功。这一页从「连不上内网的自签名服务」这个起点出发。

flowchart TB
  bad["证书不被信任"] --> d["关掉全部校验"]
  d --> nv["NoVerifier"]
  nv --> any["谁都接受 含冒名者"]
  bad --> h["只关主机名"]
  h --> berr["build 报错"]
  bad --> only["tls_certs_only"]
  only --> wp["WebPki 只信给的根"]
  wp --> ok["真服务器 通过"]
  wp --> rej["冒名者 BadSignature"]

实验 crate:exp-reqwest/pita-lab,src/bin/p08_danger.rs。一个内部 CA 给 api.internal 签了证书。三台本机 TLS 服务器(rustls,用 resolve("api.internal", addr) 指过去):

  • genuine:真服务器,证书和私钥配对。
  • wrong name:同一个 CA 签的 other.internal 证书。
  • imposter:发出的是真服务器那张公开的证书,但它没有真服务器的私钥,用自己的私钥签握手。任何能看到一次握手的人都能拿到那张证书。

每台服务器收到请求就打印 Authorization 头,客户端每次都带 bearer_auth("s3cret")。M2 Max、macOS 27、rustc 1.99.0,release。

现象

== server: genuine
      [genuine server] got authorization=Some("Bearer s3cret")
    danger_accept_invalid_certs(true)                200 OK
    danger_accept_invalid_hostnames(true)            build error: builder error <- disabling rustls hostname verification only allowed with tls_certs_only()
      [genuine server] got authorization=Some("Bearer s3cret")
    tls_certs_only([ca])                             200 OK
      [genuine server] got authorization=Some("Bearer s3cret")
    tls_certs_only([ca]) + accept_invalid_hostnames  200 OK
== server: wrong name
      [wrong name server] got authorization=Some("Bearer s3cret")
    danger_accept_invalid_certs(true)                200 OK
    danger_accept_invalid_hostnames(true)            build error: builder error <- disabling rustls hostname verification only allowed with tls_certs_only()
    tls_certs_only([ca])                             error: invalid peer certificate: certificate not valid for name "api.internal"; certificate is only valid for DnsName("other.internal")
      [wrong name server] got authorization=Some("Bearer s3cret")
    tls_certs_only([ca]) + accept_invalid_hostnames  200 OK
== server: imposter
      [imposter server] got authorization=Some("Bearer s3cret")
    danger_accept_invalid_certs(true)                200 OK
    danger_accept_invalid_hostnames(true)            build error: builder error <- disabling rustls hostname verification only allowed with tls_certs_only()
    tls_certs_only([ca])                             error: invalid peer certificate: BadSignature
    tls_certs_only([ca]) + accept_invalid_hostnames  error: invalid peer certificate: BadSignature
  • danger_accept_invalid_certs(true) 三台都接受,冒名者也拿到了 Bearer s3cret。它没有私钥,只是抄了一张公开的证书。
  • danger_accept_invalid_hostnames(true) 单独用,build 就失败。
  • tls_certs_only([ca]) 只接受真服务器;加上 accept_invalid_hostnames 以后接受了名字不对的那台,冒名者仍然被 BadSignature 挡住。

原因

danger_accept_invalid_certs 装的是一个什么都不验的校验器。 danger_accept_invalid_certs 转调 tls_danger_accept_invalid_certs,把 certs_verification 设成假;build 时选校验器的第一个分支就是它,装上 NoVerifier。NoVerifier 的 verify_server_cert、verify_tls12_signature、verify_tls13_signature 全部直接返回成功。

名字叫「接受无效证书」,文档的警告也只说「any certificate for any site will be trusted」,读起来像是「证书不检查了,但连接还是和那台服务器建的」。实际上第三个方法也跳过了:TLS 1.3 的 CertificateVerify 是服务器用证书对应的私钥对握手签名,证明「对面持有这张证书的私钥」。不验它,抄一张公开证书就够了,冒名者那一组的 200 OK 就是这么来的。

只关主机名要换一个校验器,所以有前提。 danger_accept_invalid_hostnames(true) 让 build 选 IgnoreHostname:链照样追到根、握手签名照样验,只跳过名字比对。它是 reqwest 自己写的校验器,信任锚只能是你给的根证书,拿不到平台校验器背后的系统信任库,所以没有 tls_certs_only 时 build 直接返回上面那个 builder error。用 native-tls 后端时两个开关原样交给系统 TLS 库,没有这个限制。

📝NOTE
冷知识:rustls 下的 danger_accept_invalid_hostnames 是 0.12.6 才有的

#2249「feat: Implement accept_invalid_hostnames for rustls」(c660535,首次发布在 v0.12.6)。之前 danger_accept_invalid_hostnames 这个方法本身、Config 里的 hostname_verification 字段都包在 #[cfg(feature = "native-tls")] 里,只开 rustls 的项目根本调不到它;rustls 后端只有「全验」和 NoVerifier「全不验」两档,想只放过名字就只能全关。这个提交把它们改成 #[cfg(feature = "__tls")],在 tls.rs 里新写了 IgnoreHostname,信任锚用的是当时 rustls 分支自己拼的 root_cert_store(用户给的根,加上按 feature 放进来的 webpki-roots 或系统根)。到了 0.13.0,#2891(d506978)把默认校验交给平台校验器,root_cert_store 没有了,IgnoreHostname 改用 rustls_store(config.root_certs),只剩用户给的根;同一个 diff 加上了 if !config.tls_certs_only 的检查和那句注释「Should this just warn? Error for now...」。所以同样一行 danger_accept_invalid_hostnames(true),0.12.6 到 0.12.28 能用,0.13 起 build 报错。

修法

连不上自己的服务,就告诉客户端该信谁,而不是谁都信:

  • 有内部 CA:tls_certs_only([ca]),只信这个 CA(上面第三行);还要访问公网,用 tls_certs_merge([ca]),在系统根之外再加它。
  • 只有一张自签名证书:把这张证书本身交给 tls_certs_only,TLS 配置 的「本机自签名服务器怎么连」实测过。
  • 证书是对的、只是名字对不上(用 IP 访问、内网别名):tls_certs_only([ca]) 加 danger_accept_invalid_hostnames(true)。链和签名照验,冒名者照样被挡(上面最后一行)。更好的做法是给证书补上 SAN,或者用 resolve() 把证书上的名字指到实际地址。

非要 danger_accept_invalid_certs 不可(一次性的本地调试), 把它放在显式的开关后面,并且只在调试构建里存在,确保它到不了生产配置。

怎么检查

  • 搜代码: grep -rn 'danger_accept_invalid\|tls_danger_' src/,0.13 两套名字都能编译,两个都要搜。每一处都要能说清楚为什么,以及为什么不能用 tls_certs_only。
  • 找出它是从哪里来的: 常见来源是读配置的代码(insecure = true)、环境变量开关、或者「测试里能用就照搬到了生产」。
  • 写一个冒名者测试: 本页的 imposter 服务器就是测试写法:发出真服务器的证书,用另一把私钥。任何 TLS 配置连上它都应该失败。

小结

  • danger_accept_invalid_certs(true) 装上 NoVerifier:不查证书链、不查名字,也不验握手签名;抄了公开证书、没有私钥的服务器照样拿到请求里的凭据。
  • danger_accept_invalid_hostnames(true) 只跳过名字,链和签名照验;rustls 后端下必须配 tls_certs_only,否则 build 报错(0.13 起)。
  • 连自签名或内部 CA 的服务,用 tls_certs_only / tls_certs_merge 把根证书交给客户端,不要关校验。

💬 关联讨论

发起讨论
加载中...