Rust in Everything
基于 reqwest 0.13.5

TLS 配置

主线位置:连接主线的 TLS 一步,但它的全部决定都提前在 ClientBuilder::build 里做完了:用哪个后端、哪个 crypto provider、哪个证书校验器、ALPN 报哪几个协议。到了连接器里,Inner 只是拿着建好的 ClientConfig 去握手。

flowchart TB
  cfg["config.tls"] --> nt["NativeTls"]
  cfg --> bnt["BuiltNativeTls"]
  cfg --> rs["Rustls"]
  cfg --> brs["BuiltRustls"]
  cfg --> unk["Unknown:build 报错"]
  rs --> prov["provider"]
  prov --> ver{"校验器"}
  ver -- 不校验证书 --> nov["NoVerifier"]
  ver -- 不校验主机名 --> ign["IgnoreHostname"]
  ver -- 默认 --> plat["平台校验器"]
  ver -- tls_certs_only --> wpk["WebPki 自带根"]
  nov --> alpn["ALPN"]
  ign --> alpn
  plat --> alpn
  wpk --> alpn
  alpn --> conn["new_rustls_tls"]
  brs --> conn

源码:类型在 tls.rs(922 行,近一半是文档注释和测试),组装在 async_impl/client.rs 的 build 里(TlsBackend 的 match 约 370 行),握手在 connect.rs。握手本身、证书路径构建、provider 的内部都是 rustls 的事,见 rustls 卷;本页只讲 reqwest 怎么配置它。

实验平台:M2 Max、macOS 27、rustc 1.99.0,reqwest 0.13.5,release 构建,所有客户端 .no_proxy()。服务器是同一进程里的 tokio-rustls 服务器(ring provider),证书由 rcgen 现签:一个测试 CA 签发的 api.test 叶子证书。客户端用 .resolve("api.test", addr) 把这个名字指到本机(见 DNS),SNI 和证书校验都按 api.test 进行。服务器在把连接交给 rustls 之前,先 peek 出 ClientHello 原始字节逐字段拆开,握手后打印协商出的版本、密钥交换组和 ALPN。

后端:feature 决定有什么,TlsBackend 决定用什么

Cargo.toml 里相关的 feature:

default = ["default-tls", "charset", "http2", "system-proxy"]

default-tls = ["rustls"]

rustls = ["__rustls-aws-lc-rs", "dep:rustls-platform-verifier", "__rustls"]
rustls-no-provider = ["dep:rustls-platform-verifier", "__rustls"]

native-tls = ["__native-tls", "__native-tls-alpn"]
native-tls-no-alpn = ["__native-tls"]

0.13 的默认是 rustls,带 aws-lc-rs provider 和 rustls-platform-verifier;0.12 的默认是 native-tls。CHANGELOG 的 v0.13.0 一节把这列为第一条破坏性变更,同时:rustls-tls 改名为 rustls,选根证书来源的几个 rustls-tls-*-roots feature 全部删掉,native-tls 默认带 ALPN。

一个 Client 用哪个后端由 config.tls 决定,它的默认值是 TlsBackend::default():

impl Default for TlsBackend {
    fn default() -> TlsBackend {
        #[cfg(any(
            all(feature = "__rustls", not(feature = "__native-tls")),
            feature = "http3"
        ))]
        {
            TlsBackend::Rustls
        }

        #[cfg(all(feature = "__native-tls", not(feature = "http3")))]
        {
            TlsBackend::NativeTls
        }
    }
}

只要 native-tls 编进来了(且没开 http3),默认就是 native-tls,不管 default-tls 是不是也开着。依赖树里任何一个 crate 打开 reqwest 的 native-tls,你那个没做任何配置的 Client::new() 就换了 TLS 实现。另起一个 crate,在模板的 feature 之外再打开 native-tls,同一个服务器:

== default builder
ClientHello: 186 bytes on the wire (record len 181)
  session_id 0B, 22 cipher suites, extensions 92B
  server_name: api.test
  supported_groups: ["secp256r1", "secp384r1", "secp521r1"]
  signature_algorithms: 8 schemes
  alpn: ["h2", "http/1.1"]
  extension ids: [0, 10, 11, 13, 16, 5, 18, 23]
server: negotiated TLSv1_2, kx Some(secp256r1), alpn Some("h2")
client: 200 OK HTTP/2.0, tls version None, peer cert 343 bytes
== tls_backend_rustls()
ClientHello: 263 bytes on the wire (record len 258)
  session_id 32B, 10 cipher suites, extensions 161B
  signature_algorithms: 13 schemes
  supported_groups: ["X25519", "secp256r1", "secp384r1", "X25519MLKEM768"]
  alpn: ["h2", "http/1.1"]
  server_name: api.test
  key_share (38B): X25519:32B
  supported_versions: ["0304", "0303"]
  extension ids: [11, 45, 5, 13, 10, 16, 0, 51, 23, 35, 43]
server: negotiated TLSv1_3, kx Some(X25519), alpn Some("h2")
client: 200 OK HTTP/2.0, tls version Some(Version(Tls1_3)), peer cert 343 bytes

默认的 Client::builder() 走了 native-tls(macOS 上是 Security.framework):没有 supported_versions 和 key_share 扩展,只能谈 TLS 1.2,TlsInfo 里也拿不到版本。和 0.12 不同的是它带了 ALPN,协商出了 HTTP/2。显式调 tls_backend_rustls() 才回到 rustls。tls.rs 的模块文档也提醒了这一点:「If you wish to ensure your Client uses a specific backend, call the appropriate builder methods」。

本站用的是 0.12.28,两种 TLS 都编进来了,实测请求走 native-tls、ClientHello 169 字节且没有 ALPN(rustls 卷的 本站的 rustls)。升级到 0.13 后这条规则不变,只要 native-tls 还在依赖树里,默认就还是它,见 本站的 reqwest。

📝NOTE
冷知识:换默认后端时,特意留了「native-tls 优先」

#2752(「make Rustls the default TLS provider」,首次发布在 v0.13.0)的说明只有两句,第二句是:「It keeps the native-tls feature with the same configuration it had before as the default TLS provider.」diff 里,0.12 的 default-tls 本来就等于 native-tls 那几个依赖,TlsBackend 的默认变体叫 Default;这个提交让 default-tls = ["rustls-tls"],把 native-tls 的依赖挪给 native-tls feature,变体改名 NativeTls,Default 实现里 native-tls 的分支保持优先。用意是:明确打开 native-tls 的项目,升级后行为不变。代价就是上面的实验:这个「明确打开」可以来自任何一个依赖。

tls.rs 的几个类型

  • Certificate:一张根证书。两个后端都编进来时它同时持有两份:native 是 from_der / from_pem 时就解析好的 native_tls::Certificate,original 是 rustls 用的原始字节(Cert::Der 或 Cert::Pem),到 build 时才解析。所以只开 rustls 时,一张坏证书要到 build() 才报错。from_pem_bundle 一次读出多张。
  • Identity:客户端证书。内部枚举 ClientCert 有 Pkcs12、Pkcs8(native-tls)和 Pem(rustls)三种,后端和格式对不上时 build 报错。客户端证书在握手里的流程见 rustls 卷的 客户端证书与自定义校验。
  • Version:TLS_1_0 到 TLS_1_3 四个常量,内部枚举 #[non_exhaustive]。tls_version_min / max 在 rustls 分支里过滤 rustls::ALL_VERSIONS(rustls 只实现了 1.2 和 1.3),过滤空了就报 empty supported tls versions。
  • TlsBackend:上面那张图的五个分支。Built* 两个来自 tls_backend_preconfigured,UnknownPreconfigured 表示传进来的类型认不出来。
  • TlsInfo:响应上的对端证书和 TLS 版本,见连接器。

crypto provider:先看进程默认,再用 aws-lc-rs

TlsBackend::Rustls 分支先按版本过滤,然后取 provider:

                    // Allow user to have installed a runtime default.
                    // If not, we ship with _our_ recommended default.
                    let provider = rustls::crypto::CryptoProvider::get_default()
                        .map(|arc| arc.clone())
                        .unwrap_or_else(default_rustls_crypto_provider);

default_rustls_crypto_provider 在 rustls feature 下返回 aws_lc_rs::default_provider();在 rustls-no-provider 下直接 panic,报错原文是「No rustls crypto provider is configured. When using the rustls-no-provider feature you must install a crypto provider before building a Client. ...」。provider 随后用 ClientConfig::builder_with_provider 传进去,不走 rustls 的 ClientConfig::builder(),所以 ring 和 aws-lc-rs 两个 rustls feature 同时打开也不会 panic(那个 panic 见 rustls 卷 CryptoProvider)。

进程默认 provider 优先。 程序里任何地方(包括某个依赖)调过 install_default,之后建的 reqwest 客户端就用它。这在 build() 时读取,已经建好的客户端不受影响。

默认的 ClientHello

默认 Client::builder(),不加任何 TLS 配置,连 api.test(证书由测试 CA 签发,平台校验器不认识它,所以握手会失败,但 ClientHello 已经发出去了):

== A default
ClientHello: 263 bytes on the wire (record len 258)
  session_id 32B, 10 cipher suites, extensions 161B
  supported_versions: ["0304", "0303"]
  key_share (38B): X25519:32B
  alpn: ["h2", "http/1.1"]
  signature_algorithms: 13 schemes
  server_name: api.test
  supported_groups: ["X25519", "secp256r1", "secp384r1", "X25519MLKEM768"]
  extension ids: [43, 5, 51, 35, 16, 11, 45, 13, 0, 10, 23]
server: handshake failed: received fatal alert: UnknownCA
client error: error sending request for url (https://api.test:62071/) <- client error (Connect) <- invalid peer certificate: UnknownIssuer

连跑 3 次都是 263 字节,扩展顺序每次不同(rustls 0.23 起随机排列,见 rustls 卷 客户端 TLS 1.3)。

  • 只有一个 32 字节的 X25519 密钥共享。 X25519MLKEM768 出现在 supported_groups 里,但排在最后,也没有为它发密钥共享。原因在 reqwest 对 rustls 的依赖:rustls = { version = "0.23.4", optional = true, default-features = false, features = ["std", "tls12"] },关掉了 rustls 的默认 feature,其中就有 prefer-post-quantum。aws-lc-rs 的 DEFAULT_KX_GROUPS 在这个 feature 关着时把混合组排在最后,initial_key_share 只为第一个组生成密钥。只依赖 reqwest 的新项目,cargo tree -e features -i rustls 显示 rustls 只开了 aws-lc-rs、aws_lc_rs、std、tls12 四个 feature。rustls 卷用 rustls 默认 feature 测到的 aws-lc-rs ClientHello 是 1466 字节、两个密钥共享,reqwest 的默认配置不是那样。
  • signature_algorithms 13 个:来自 aws-lc-rs provider 的签名校验算法表(平台校验器和下面的 WebPki 校验器都用它)。
  • ALPN 是 h2、http/1.1,见下文。

MLKEM 只是「提供」,不是「首选」。服务器换成只接受 X25519MLKEM768 的 aws-lc-rs 配置,和「X25519MLKEM768 优先、也接受 X25519」的配置:

== server kx [X25519MLKEM768]
server: negotiated TLSv1_3, kx Some(X25519MLKEM768), alpn Some("h2")
client: 200 OK HTTP/2.0 after 32ms
== server kx [X25519MLKEM768, X25519]
server: negotiated TLSv1_3, kx Some(X25519), alpn Some("h2")
client: 200 OK HTTP/2.0 after 2ms

第一种协商出了 X25519MLKEM768:第一个 ClientHello 里没有这个组的密钥共享,服务器只能回 HelloRetryRequest,客户端补发第二个 ClientHello,多一个往返(HelloRetryRequest 的流程见 rustls 卷 客户端 TLS 1.3)。第二种,rustls 服务器选了客户端已经给出密钥共享的 X25519,不做 HelloRetryRequest,结果是经典密钥交换。毫秒数只跑了一次,第一种里还包含第一次用 aws-lc-rs 的开销,不作比较。

❗IMPORTANT
动脑筋:在建客户端之前先执行 rustls::crypto::ring::default_provider().install_default(),其余配置不变(tls_certs_only(ca)),reqwest 发出的 ClientHello 有多少字节?supported_groups 和 signature_algorithms 会怎么变?

证书校验器:四选一

provider 定了以后选校验器,按优先级:

                    let config_builder = if !config.certs_verification {
                        config_builder
                            .dangerous()
                            .with_custom_certificate_verifier(Arc::new(NoVerifier))
                    } else if !config.hostname_verification {
                        if !config.tls_certs_only {
                            // Should this just warn? Error for now...
                            return Err(crate::error::builder(
                                    "disabling rustls hostname verification only allowed with tls_certs_only()"
                            ));
                        }

                        config_builder
                            .dangerous()
                            .with_custom_certificate_verifier(Arc::new(IgnoreHostname::new(
                                crate::tls::rustls_store(config.root_certs)?,
                                signature_algorithms,
                            )))
                    } else if !config.tls_certs_only {
                        // ...
                        let verifier = if config.root_certs.is_empty() {
                            rustls_platform_verifier::Verifier::new(provider)
                                .map_err(crate::error::builder)?
                        } else {
                            // ...
                                rustls_platform_verifier::Verifier::new_with_extra_roots(
                                    crate::tls::rustls_der(config.root_certs)?,
                                    provider,
                                )
                                .map_err(crate::error::builder)?
                            // ...
                        };
                        // ...
                    } else {
                        if config.crls.is_empty() {
                            config_builder.with_root_certificates(crate::tls::rustls_store(
                                config.root_certs,
                            )?)
                        } else {
                            // ... WebPkiServerVerifier with CRLs
                        }
                    };
配置校验器信任谁
默认rustls_platform_verifier::Verifier::new操作系统的信任库(macOS 上交给 Security.framework)
tls_certs_merge(certs)Verifier::new_with_extra_roots系统信任库加你给的根;只在非 Android 的 Unix 和 Windows 上可用,其余平台 build 报错
tls_certs_only(certs)rustls 的 WebPkiServerVerifier只有你给的根;tls_crls_only 的吊销列表只能和它一起用
tls_danger_accept_invalid_hostnames(true)IgnoreHostname只有你给的根,必须配 tls_certs_only
tls_danger_accept_invalid_certs(true)NoVerifier谁都信

0.13 删掉了 webpki-roots。#2891(「consolidate TLS options with rustls-platform-verifier」,首次发布在 v0.13.0)的说明给了替代做法:「instead of adding webpki-roots with a feature flag, you should use the webpki-roots crate yourself, and pass them into tls_certs_only(certs)」。add_root_certificate 还在,是 tls_certs_merge 的旧名(只做了文档上的软弃用)。

本机自签名服务器怎么连

三个服务器:wrong-name(测试 CA 签发、但证书名字是 other.test),self-signed(rcgen::generate_simple_self_signed 生成的 api.test 自签名证书,没有 CA),imposter(发出真正的、测试 CA 签发的 api.test 证书链,但手里的私钥是另一把,CertifiedKey::new 不检查两者是否配对)。每个服务器用 8 种客户端配置各连一次,服务端日志行已滤掉:

== server: wrong-name
  default: error: invalid peer certificate: UnknownIssuer
  tls_certs_merge(ca): error: invalid peer certificate: NotValidForName
  tls_certs_only(ca): error: invalid peer certificate: certificate not valid for name "api.test"; certificate is only valid for DnsName("other.test")
  tls_certs_merge(self-signed): error: invalid peer certificate: UnknownIssuer
  tls_certs_only(self-signed): error: invalid peer certificate: UnknownIssuer
  accept_invalid_hostnames: build error: reqwest::Error { kind: Builder, source: "disabling rustls hostname verification only allowed with tls_certs_only()" }
  accept_invalid_hostnames + certs_only(ca): 200 OK
  accept_invalid_certs: 200 OK
== server: self-signed
  default: error: invalid peer certificate: Other(OtherError("“rcgen self signed cert” certificate is not trusted: -67843"))
  tls_certs_merge(ca): error: invalid peer certificate: Other(OtherError("“rcgen self signed cert” certificate is not trusted: -67843"))
  tls_certs_only(ca): error: invalid peer certificate: UnknownIssuer
  tls_certs_merge(self-signed): 200 OK
  tls_certs_only(self-signed): 200 OK
  accept_invalid_hostnames: build error: reqwest::Error { kind: Builder, source: "disabling rustls hostname verification only allowed with tls_certs_only()" }
  accept_invalid_hostnames + certs_only(ca): error: invalid peer certificate: UnknownIssuer
  accept_invalid_certs: 200 OK
== server: imposter
  default: error: invalid peer certificate: UnknownIssuer
  tls_certs_merge(ca): error: invalid peer certificate: BadSignature
  tls_certs_only(ca): error: invalid peer certificate: BadSignature
  tls_certs_merge(self-signed): error: invalid peer certificate: UnknownIssuer
  tls_certs_only(self-signed): error: invalid peer certificate: UnknownIssuer
  accept_invalid_hostnames: build error: reqwest::Error { kind: Builder, source: "disabling rustls hostname verification only allowed with tls_certs_only()" }
  accept_invalid_hostnames + certs_only(ca): error: invalid peer certificate: BadSignature
  accept_invalid_certs: 200 OK

连本机自签名服务器,正确的做法是 self-signed 那一组的第 4、5 行:把服务器证书本身当根证书交给 tls_certs_merge 或 tls_certs_only。在这台 Mac 上平台校验器接受了额外的根(CA 和自签名叶子都行),不需要把证书装进钥匙串。两者的区别是 merge 还信任系统根,only 只信你给的。

错误文本也透露了是谁在校验:平台校验器把 Security.framework 的错误码原样带出来(-67843),WebPki 校验器给出的是 rustls 的 NotValidForName 详细形式。

danger_accept_invalid_certs:NoVerifier 什么都不验

impl ServerCertVerifier for NoVerifier {
    fn verify_server_cert(
        &self,
        _end_entity: &rustls_pki_types::CertificateDer,
        _intermediates: &[rustls_pki_types::CertificateDer],
        _server_name: &ServerName,
        _ocsp_response: &[u8],
        _now: UnixTime,
    ) -> Result<ServerCertVerified, TLSError> {
        Ok(ServerCertVerified::assertion())
    }

    fn verify_tls12_signature(
        &self,
        _message: &[u8],
        _cert: &rustls_pki_types::CertificateDer,
        _dss: &DigitallySignedStruct,
    ) -> Result<HandshakeSignatureValid, TLSError> {
        Ok(HandshakeSignatureValid::assertion())
    }

    fn verify_tls13_signature(
        // ...
    ) -> Result<HandshakeSignatureValid, TLSError> {
        Ok(HandshakeSignatureValid::assertion())
    }
    // ...
}

NoVerifier 的三个方法都直接返回成功。所以 danger_accept_invalid_certs(true) 关掉的不只是「证书链能不能追到根」:

  • 主机名也不查(_server_name 没用到):wrong-name 那一组最后一行 200 OK。
  • 握手签名也不验(verify_tls13_signature 直接成功):imposter 那一组最后一行 200 OK。服务器发出一张别人的真证书,却并不持有它的私钥,CertificateVerify 签名是用另一把钥匙签的,客户端照样接受。任何能拦截流量的中间人,都可以拿目标网站公开的证书冒充它。rustls 卷 客户端证书与自定义校验 讲了 no-op 校验器的这个后果,这里是它在 reqwest 上的实测。

supported_verify_schemes 返回一张固定的 13 项表,连 RSA_PKCS1_SHA1 和 ED448 都在里面,反正不验。native-tls 分支则是把两个开关原样传给 TlsConnectorBuilder::danger_accept_invalid_certs / danger_accept_invalid_hostnames。

danger_accept_invalid_hostnames:IgnoreHostname 只跳过名字

IgnoreHostname 的 verify_server_cert:

        let cert = ParsedCertificate::try_from(end_entity)?;

        rustls::client::verify_server_cert_signed_by_trust_anchor(
            &cert,
            &self.roots,
            intermediates,
            now,
            self.signature_algorithms.all,
        )?;
        Ok(ServerCertVerified::assertion())

证书链照样追到根、照样查有效期;两个签名方法调 rustls 的 verify_tls12_signature / verify_tls13_signature,握手签名照样验。跳过的只有名字比对。实验里它接受了 wrong-name,拒绝了 self-signed(UnknownIssuer)和 imposter(BadSignature)。

📝NOTE
没有蠢问题

问: 为什么只关主机名校验,还非得配 tls_certs_only?不配就直接 build 报错,danger_accept_invalid_certs 却没有这个限制。

答: 因为要换掉的是校验器本身。默认的平台校验器把整件事交给操作系统,没有「只跳过名字」的开关;IgnoreHostname 是 reqwest 自己写的校验器,它的信任锚是 rustls_store(config.root_certs),也就是只有你给的根,系统根它拿不到。不配 tls_certs_only 的话,这个校验器会悄悄地什么都不信任,或者 reqwest 得偷偷换成「信任所有人」,两样都不对,所以源码里写着「Should this just warn? Error for now...」,选择报错。native-tls 后端能把开关交给系统 TLS 库,所以没有这个限制。

ALPN 决定 HTTP 版本

rustls 分支最后设 ALPN:

                    // ALPN protocol
                    match config.http_version_pref {
                        HttpVersionPref::Http1 => {
                            tls.alpn_protocols = vec!["http/1.1".into()];
                        }
                        #[cfg(feature = "http2")]
                        HttpVersionPref::Http2 => {
                            tls.alpn_protocols = vec!["h2".into()];
                        }
                        #[cfg(feature = "http3")]
                        HttpVersionPref::Http3 => {
                            // h3 ALPN is not valid over TCP
                        }
                        HttpVersionPref::All => {
                            tls.alpn_protocols = vec![
                                #[cfg(feature = "http2")]
                                "h2".into(),
                                "http/1.1".into(),
                            ];
                        }
                    }

默认 All:报 h2、http/1.1。http1_only() 只报 http/1.1;http2_prior_knowledge() 只报 h2,同时让 hyper-util 的 Client 只用 HTTP/2。关掉 http2 feature 时 h2 不在列表里。握手后服务器选中的协议回到 RustlsTlsConn::connected(),选中 h2 才调 negotiated_h2()(连接器)。同一个支持 h2 的服务器,默认和 http1_only() 两种:

== C tls_certs_only(ca)
ClientHello: 263 bytes on the wire (record len 258)
...
  alpn: ["h2", "http/1.1"]
...
server: negotiated TLSv1_3, kx Some(X25519), alpn Some("h2")
client: 200 OK HTTP/2.0, tls version Some(Version(Tls1_3)), peer cert 343 bytes
== D tls_certs_only(ca) + http1_only
ClientHello: 260 bytes on the wire (record len 255)
...
  alpn: ["http/1.1"]
...
server: negotiated TLSv1_3, kx Some(X25519), alpn Some("http/1.1")
client: 200 OK HTTP/1.1, tls version Some(Version(Tls1_3)), peer cert 343 bytes

少了 h2 这 3 个字节的协议名,ClientHello 小 3 字节,结果是 HTTP/1.1。HTTP/2 连接在池里怎么共享见连接池与 HTTP 版本。native-tls 分支在 native-tls feature 下按同样的规则调 request_alpns;0.12 里 native-tls 默认不带 ALPN(要 native-tls-alpn feature),0.13 起默认带上(e8d89f4,「enable ALPN by default in native-tls」,#2907,首次发布在 v0.13.0),想去掉用 native-tls-no-alpn。

build 里剩下几个 rustls 选项都是直接赋值:tls_sni(false) 设 enable_sni = false;tls_sslkeylogfile(true)(0.13.4 新增)设 key_log = KeyLogFile::new(),运行时有 SSLKEYLOGFILE 环境变量才写文件。

tls_backend_preconfigured:你给什么就用什么

旧名 use_preconfigured_tls。tls_backend_preconfigured 收一个 impl Any,试着 downcast 成 native_tls::TlsConnector 或 rustls::ClientConfig,都不是就记成 UnknownPreconfigured,等 build 时报错。BuiltRustls 分支直接把你的 ClientConfig 交给 new_rustls_tls,上面那一整段(provider、校验器、ALPN、SNI、版本过滤、danger 开关)一行都不执行。用 ring provider 建一个只信测试 CA、不设 ALPN 的 ClientConfig:

A ClientConfig, no ALPN: 200 OK HTTP/1.1
B ClientConfig, ALPN h2+http/1.1: 200 OK HTTP/2.0
C ClientConfig + http1_only(): 200 OK HTTP/1.1
D ClientConfig + danger_accept_invalid_certs, untrusted server: error: invalid peer certificate: UnknownIssuer
E Arc<ClientConfig>: build error: builder error <- Unknown TLS backend passed to `use_preconfigured_tls`
  • A:服务器支持 h2,但你的配置没设 ALPN,reqwest 也不替你设,结果是 HTTP/1.1。要 HTTP/2 得自己填 alpn_protocols(B)。
  • D:danger_accept_invalid_certs(true) 被忽略,校验器是你配置里的那个。
  • E:传 Arc<ClientConfig> 而不是 ClientConfig 时 downcast 失败,到 build() 才报错,错误信息还是旧方法名。文档注释也提醒,传进来的类型必须和 reqwest 用的是同一个版本的 rustls / native-tls,否则同样是 Unknown。

动脑筋答案

== E ring installed as process default, tls_certs_only(ca)
ClientHello: 253 bytes on the wire (record len 248)
  session_id 32B, 10 cipher suites, extensions 151B
  alpn: ["h2", "http/1.1"]
  key_share (38B): X25519:32B
  supported_groups: ["X25519", "secp256r1", "secp384r1"]
  server_name: api.test
  supported_versions: ["0304", "0303"]
  signature_algorithms: 9 schemes
  extension ids: [16, 51, 11, 10, 23, 0, 35, 45, 5, 43, 13]
server: negotiated TLSv1_3, kx Some(X25519), alpn Some("h2")
client: 200 OK HTTP/2.0, tls version Some(Version(Tls1_3)), peer cert 343 bytes

253 字节,比默认少 10 字节。build 时 CryptoProvider::get_default() 返回了刚装的 ring,reqwest 就用它,不再用自己的 aws-lc-rs:

  • supported_groups 少了 X25519MLKEM768(2 字节):ring provider 没有这个组。
  • signature_algorithms 从 13 个变成 9 个(少 8 字节):WebPki 校验器的算法表来自 provider,ring 的表短。

密钥共享仍是一个 X25519,所以大小只差这两处。依赖里有谁在启动时装了进程默认 provider,你的 reqwest 客户端就跟着它走。

小结

  • 0.13 默认 rustls(aws-lc-rs + 平台校验器);但只要 native-tls 编进来且没开 http3,TlsBackend::default() 就是 native-tls,这是 #2752 有意保留的;要确定用哪个,显式调 tls_backend_rustls() / tls_backend_native()。
  • rustls 的 provider 先取进程默认,没有才用 aws-lc-rs;reqwest 关了 rustls 的默认 feature,prefer-post-quantum 不在,默认 ClientHello 263 字节、只带 X25519 密钥共享,X25519MLKEM768 排在 supported_groups 末尾,要用它得多一次 HelloRetryRequest。
  • 校验器四选一:平台校验器(tls_certs_merge 加根)、tls_certs_only 的 WebPki、IgnoreHostname(只跳过名字,必须配 tls_certs_only)、NoVerifier(证书链、主机名、握手签名全不验,实测接受了不持有私钥的冒充者)。webpki-roots 已删,要用就自己传给 tls_certs_only。
  • 本机自签名服务器:把它的证书交给 tls_certs_merge 或 tls_certs_only,不要用 danger 开关。
  • ALPN 默认 h2、http/1.1,http1_only / http2_prior_knowledge 各留一个;tls_backend_preconfigured 跳过 reqwest 的全部 TLS 配置,包括 ALPN,传错类型(如 Arc<ClientConfig>)到 build 才报错。

💬 关联讨论

发起讨论
加载中...