Rust in Everything
基于 rustls 0.23.46

Could not automatically determine the process-level CryptoProvider

主线位置:握手主线开始之前。ClientConfig / ServerConfig 构建时要拿到一个 CryptoProvider,不显式传就去读进程级默认值,读不到再按 crate feature 推断。推断的规则、install_default 和 OnceLock 的关系见 CryptoProvider。本页讲推断失败时的 panic、它常见的来路和三种修法。

flowchart TB
  app["应用 rustls ring"] --> uni["feature 并集"]
  dep["依赖 默认 feature"] --> uni
  uni --> both["ring 加 aws_lc_rs"]
  both --> b["builder()"]
  b --> gd{"进程默认已装"}
  gd -->|是| ok["用已装的"]
  gd -->|否| inf["按 feature 推断"]
  inf --> boom["None 然后 panic"]
  fix1["main 里 install_default"] --> gd
  fix2["builder_with_provider"] --> ok

实验 crate:exp-rustls/pita-feat(src/main.rs,run.sh 跑全部 feature 组合)。M2 Max、macOS 27、rustc 1.99.0,release。

现象

应用自己只要 ring,Cargo.toml 写得很规矩:

[dependencies]
rustls = { version = "=0.23.46", default-features = false, features = ["ring", "std", "tls12", "logging"] }
tokio-rustls = "0.26"

第二行是一个没关默认 feature 的依赖,这里用 tokio-rustls 0.26 当例子,它的默认 feature 是 logging、tls12、aws_lc_rs。main 里第一件事是 ClientConfig::builder():

app features: rustls-defaults=false pq=false dep-defaults=true dep-ring=false install=false explicit=false

thread 'main' (7813525) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustls-0.23.46/src/crypto/mod.rs:249:14:

Could not automatically determine the process-level CryptoProvider from Rustls crate features.
Call CryptoProvider::install_default() before this point to select a provider manually, or make sure exactly one of the 'aws-lc-rs' and 'ring' features is enabled.
See the documentation of the CryptoProvider type for more information.
            
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
exit=101
  • 报错位置在 rustls 的 crypto/mod.rs:249,不在调用 builder() 的那一行。
  • 应用代码一个字没改,只是多了一个依赖。tokio-rustls 本身完全没被调用。
  • 同一个程序换几种依赖写法(run.sh,八种组合):
依赖图里的写法结果
只有上面第一行config ok: secure_random Ring,退出码 0
加 tokio-rustls = "0.26"(默认 feature)panic,101
rustls 那行不写 default-features = falsepanic,101
rustls 加 prefer-post-quantumpanic,101
加 tokio-rustls,default-features = false, features = ["ring", ...]config ok: secure_random Ring,0

后两种 panic 是同一件事从另一个方向进来,见 ring 加 prefer-post-quantum 带进 aws-lc-rs。

0.23.0 到 0.23.29 打印的是旧的一行文案 no process-level CryptoProvider available -- call CryptoProvider::install_default() before this point,按报错搜索时两句都要试;换文案的经过见 CryptoProvider 里的冷知识。

原因

ClientConfig::builder 转给 builder_with_protocol_versions,后者调 get_default_or_install_from_crate_features:

    pub(crate) fn get_default_or_install_from_crate_features() -> &'static Arc<Self> {
        if let Some(provider) = Self::get_default() {
            return provider;
        }

        let provider = Self::from_crate_features()
            .expect(r###"
Could not automatically determine the process-level CryptoProvider from Rustls crate features.
// ...

进程里还没人装过默认值,于是走 from_crate_features。它只在「恰好开了 ring 和 aws_lc_rs 中的一个、且没开 custom-provider」时返回 Some,两个都开就是 None,.expect 当场 panic。rustls 不替你挑:两个 provider 的密钥交换组不一样(aws-lc-rs 的默认列表多一个 X25519MLKEM768,见套件与密钥交换),悄悄选一个,握手的结果就取决于依赖图里碰巧有谁。

两个都开却不是应用写的,是因为 Cargo 的 feature 按包取并集。依赖图里只有一份 rustls 0.23.46,应用要了 ring,tokio-rustls 的默认 feature aws_lc_rs 转成了 rustls/aws_lc_rs,编出来的 rustls 两个都有。

同一个函数还被 ServerConfig::builder()、WebPkiServerVerifier::builder()、WebPkiClientVerifier::builder() 调用,这几个入口在同样的依赖图下都会 panic。库里调这些入口也算,panic 时的调用栈指向库的代码。

修法

一:从依赖图里去掉第二个 provider。 只用 ring 的项目,就让每个依赖 rustls 的 crate 都只开 ring:

tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "logging", "tls12"] }
app features: rustls-defaults=false pq=false dep-defaults=false dep-ring=true install=false explicit=false
config ok: secure_random Ring, kx_groups [X25519, secp256r1, secp384r1]
process default installed: true
exit=0

process default installed: true 说明推断成功、ring 被装成了默认值。这是最干净的修法,顺带省掉 aws-lc-rs 的编译和体积(见下一页)。前提是这个依赖把 provider 做成了可选 feature,并且你能改它那一行。

二:在 main 的开头、任何 builder() 之前装好默认值。 依赖图改不动时用这个:

fn main() {
  let r = rustls::crypto::ring::default_provider().install_default();
  // ...
}
app features: rustls-defaults=true pq=false dep-defaults=false dep-ring=false install=true explicit=false
install_default(ring) -> Ok(())
config ok: secure_random Ring, kx_groups [X25519, secp256r1, secp384r1]
process default installed: true
exit=0

装好之后 get_default 有值,推断那一步根本不会走到。两个 provider 都还编在二进制里。install_default 只有第一次成功,第二次失败时 Err 里带回的是你这次传进去的 provider,见 install_default 第二次调用失败。

三:不用进程级默认值,显式传 provider。

let p = Arc::new(rustls::crypto::ring::default_provider());
let cfg = rustls::ClientConfig::builder_with_provider(p)
  .with_safe_default_protocol_versions()?
  // ...
app features: rustls-defaults=false pq=false dep-defaults=true dep-ring=false install=false explicit=true
config ok: secure_random Ring, kx_groups [X25519, secp256r1, secp384r1]
process default installed: false
exit=0

builder_with_provider 完全绕开默认值,最后一行 false 说明进程里什么也没装。库适合用这个,或者用 builder() 跟随应用装的默认值;库自己去 install_default 会和应用抢那唯一一次机会。

📝NOTE
没有蠢问题

问: main 里已经 install_default 了,程序也能跑,为什么 cargo test 还是报同一个 panic?

答: 测试二进制的入口不是你的 main,#[test] 函数之前没人装过默认值。同一份 pita-feat(rustls-defaults,install)加两个测试,一个直接 builder(),一个先调辅助函数 let _ = rustls::crypto::ring::default_provider().install_default();:

running 2 tests
tests::builds_a_client_config --- FAILED
.
...
---- tests::builds_a_client_config stdout ----
thread 'tests::builds_a_client_config' (7806835) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustls-0.23.46/src/crypto/mod.rs:249:14:
...
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

辅助函数里的 let _ = 是有意的:多个测试在同一个进程里,只有第一个装得上,后面的返回 Err,忽略即可。每个用到 builder() 的测试开头都要调它,不能指望别的测试先跑。

怎么检查

两个 provider 是不是都开了。 看 rustls 最终的 feature:

$ cargo tree -i rustls -f '{p} [{f}]' --depth 0 --features dep-defaults
rustls v0.23.46 [aws-lc-rs,aws_lc_rs,log,logging,ring,std,tls12]

方括号里同时有 ring 和 aws_lc_rs,builder() 这类入口就会 panic(除非先装了默认值)。只开 ring 时是 [log,logging,ring,std,tls12]。

是谁开的。 反查 aws-lc-rs:

$ cargo tree -e features -i aws-lc-rs --features dep-defaults
aws-lc-rs v1.19.0
├── rustls v0.23.46
│   ├── rustls feature "aws-lc-rs"
│   │   └── rustls feature "aws_lc_rs"
│   │       └── tokio-rustls feature "aws_lc_rs"
│   │           └── tokio-rustls feature "default"
│   │               └── pita-feat v0.1.0 (~/exp-rustls/pita-feat)
...

是哪一行触发的。 panic 位置永远是 crypto/mod.rs:249,要看调用方得打开 backtrace:

$ RUST_BACKTRACE=1 cargo run --release --features dep-defaults
...
   5: <rustls::crypto::CryptoProvider>::get_default_or_install_from_crate_features
             at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustls-0.23.46/src/crypto/mod.rs:249:14
   6: <rustls::client::client_conn::ClientConfig>::builder_with_protocol_versions
             at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustls-0.23.46/src/client/client_conn.rs:339:13
   7: pita_feat::main
             at ./src/main.rs:28:5

builder() 被内联掉了,栈上直接是 builder_with_protocol_versions;第 7 帧是调用方。调用方在某个依赖里时,那一帧就是库的源码路径。

本站的 sqlx 和 lettre 只开 ring、也都不走 builder(),所以今天不会撞上;某个依赖打开 rustls 默认 feature 之后会怎样,见本站的 rustls。

小结

  • 依赖图里 rustls 同时开了 ring 和 aws_lc_rs、又没人装默认 provider 时,ClientConfig::builder()、ServerConfig::builder()、WebPki*Verifier::builder() 在 crypto/mod.rs:249 panic。
  • 常见来路是 feature 并集:应用只要 ring,某个依赖(例子里是默认 feature 的 tokio-rustls 0.26)带进了 aws-lc-rs。
  • 修法按优先级:让所有依赖都只开一个 provider;应用在 main 开头 install_default;库和不想碰全局状态的代码用 builder_with_provider。
  • 测试二进制不经过你的 main,测试里也要装。
  • 用 cargo tree -i rustls -f '{p} [{f}]' --depth 0 看 feature,-e features -i aws-lc-rs 找来源,RUST_BACKTRACE=1 找调用方。

💬 关联讨论

发起讨论
加载中...