Rust in Everything
基于 rustls 0.23.46

CryptoProvider

主线位置:握手主线开始之前。ClientConfig 和 ServerConfig 在构建时各自拿到一个 Arc<CryptoProvider>,握手主线里的每一处密码学操作都从它取实现:ClientHello 的随机数和密钥共享、服务端选套件和组、证书校验、签名私钥。本页讲这个结构有哪些字段、各被谁读,以及没有显式传入时 rustls 从哪里找一个。

flowchart TB
  prov["CryptoProvider"] --> cs["cipher_suites"]
  prov --> kx["kx_groups"]
  prov --> sva["签名校验算法"]
  prov --> rnd["secure_random"]
  prov --> kp["key_provider"]
  cs --> chello["ClientHello 套件"]
  cs --> choose["服务端选套件"]
  kx --> share["密钥共享"]
  kx --> choose
  sva --> wpv["WebPki 校验器"]
  rnd --> rand["随机数 会话 票据"]
  kp --> ck["CertifiedKey"]

实验在 exp-rustls/crypto-lab(M2 Max、macOS 27、rustc 1.99.0)。它依赖 rustls = "=0.23.46",关掉默认 feature,再用自己的 feature 把 ring、aws_lc_rs、prefer-post-quantum、custom-provider 逐个打开,这样每种组合都能单独编译运行。握手都在内存里完成,证书是 rcgen 生成的 ECDSA P-256 自签证书。

五个字段

CryptoProvider 是一个普通的结构体,字段全是 pub,派生了 Clone:

#[derive(Debug, Clone)]
pub struct CryptoProvider {
    pub cipher_suites: Vec<suites::SupportedCipherSuite>,
    pub kx_groups: Vec<&'static dyn SupportedKxGroup>,
    pub signature_verification_algorithms: WebPkiSupportedAlgorithms,
    pub secure_random: &'static dyn SecureRandom,
    pub key_provider: &'static dyn KeyProvider,
}

(文档注释已删。)两个列表的顺序就是偏好顺序,第一个优先级最高。实现都是 &'static dyn Trait:一个 provider 是一组静态对象的引用,复制它只是复制几个指针和两个 Vec。

字段谁读它什么时候
cipher_suitesConfigBuilder::with_protocol_versions 检查;客户端 emit_client_hello_for_retry 列进 ClientHello;服务端 choose_suite_and_kx_group 选一个构建配置时、每次握手
kx_groups同上三处;客户端 initial_key_share 取第一个生成密钥共享每次握手
signature_verification_algorithmswith_root_certificates 交给 WebPkiServerVerifier;WebPkiServerVerifier::builder_with_provider 同理构建校验器时拷走
secure_randomClientHelloInput::new(随机数、会话 id、扩展顺序种子);服务端的 Random 和 emit_ticket;ECH每次握手
key_providerCertifiedKey::from_der,由 with_single_cert 和 with_client_auth_cert 调用只在构建配置时

两个细节从这张表里能看出来:

  • signature_verification_algorithms 和 key_provider 只在构建时用一次。校验器拿走的是算法表的副本;with_single_cert 把私钥解析成 Arc<dyn SigningKey> 放进 CertifiedKey 之后,握手里签名走的是 SigningKey,不再经过 key_provider。换句话说,想换私钥的存放方式(HSM、远程签名),实现 SigningKey 并用 with_cert_resolver 交进去就够了,KeyProvider 只管「从 DER 字节解析私钥」这一种入口。
  • secure_random 不负责临时密钥。SecureRandom 的文档写着 SupportedKxGroup::start() 生成临时密钥所需的随机数「is not included in the interface with rustls」:rustls 的 ring 密钥交换代码在 start 里自己 SystemRandom::new()。rustls 自己要的随机字节只有协议层的那几个字段(见文末动脑筋)。

SupportedCipherSuite 是一个枚举(TLS 1.2 / TLS 1.3),里面的 Tls13CipherSuite 同时装着「是什么」(CipherSuite 编号)和「怎么做」(哈希、HKDF、AEAD 的实现对象),SupportedKxGroup 也是一样,所以这两个列表既是配置也是实现。两个内置 provider 的列表内容和顺序在套件与密钥交换。

进程级默认值

不传 provider 的入口有四类:ClientConfig::builder()、ServerConfig::builder()(以及它们的 builder_with_protocol_versions)、WebPkiServerVerifier::builder()、WebPkiClientVerifier::builder()。它们都调同一个函数,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.
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.
            "###);
        // Ignore the error resulting from us losing a race, and accept the outcome.
        let _ = provider.install_default();
        Self::get_default().unwrap()
    }

顺序是:已经装了就用装好的;没装就按 crate feature 推断一个、装上、再读回来;推断不出就 panic。「装」是往一个进程级的静态变量里写,std 下是 OnceLock<Arc<CryptoProvider>>,no_std 下是 once_cell::race::OnceBox。install_default 就是 OnceLock::set,get_default 就是 OnceLock::get。两个线程同时走到推断那一步时,后一个 install_default 失败,注释说「accept the outcome」,两个线程最后读到的是同一个 Arc。

推断只看编译期的 feature,from_crate_features:

    fn from_crate_features() -> Option<Self> {
        #[cfg(all(
            feature = "ring",
            not(feature = "aws_lc_rs"),
            not(feature = "custom-provider")
        ))]
        {
            return Some(ring::default_provider());
        }

        #[cfg(all(
            feature = "aws_lc_rs",
            not(feature = "ring"),
            not(feature = "custom-provider")
        ))]
        {
            return Some(aws_lc_rs::default_provider());
        }

        #[allow(unreachable_code)]
        None
    }

恰好开了一个内置 provider、又没开 custom-provider 时才有答案。custom-provider 这个 feature 本身没有任何代码,它唯一的作用就是让这里返回 None,防止应用以为在用自己的 provider、某处 builder() 却悄悄装上了内置的那个。

实测:每种 feature 组合下 builder() 的结果

实验程序先打印 get_default() 是否有值,再调 ClientConfig::builder(),打印它用的 secure_random 和 kx_groups:

rustls feature(都关了默认 feature,另开 std、tls12、logging)builder()推断出的 provider
ring成功Ring,组 [X25519, secp256r1, secp384r1]
aws_lc_rs成功AwsLcRs,组 [X25519, secp256r1, secp384r1, X25519MLKEM768]
aws_lc_rs + prefer-post-quantum成功AwsLcRs,组 [X25519MLKEM768, X25519, secp256r1, secp384r1]
ring + aws_lc_rspanic,退出码 101—
ring + prefer-post-quantumpanic,退出码 101—
ring + custom-providerpanic,退出码 101—
一个都不开panic,退出码 101—

四个 panic 的输出完全一样:

features: ring=true aws=true pq=false custom=false
get_default() before: false

thread 'main' (7327147) 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

报错的位置是 crypto/mod.rs:249,也就是那个 .expect(...),而不是调用 builder() 的那一行;消息前后各有一个换行,末尾还带着原始字符串里的缩进空格。

最容易踩的是第五行:只想要 ring、又顺手打开了 prefer-post-quantum。Cargo.toml 里这个 feature 写的是 prefer-post-quantum = ["aws_lc_rs"],它会把 aws-lc-rs 一起打开。cargo tree -e features -i aws-lc-rs 把这条链打印出来:

aws-lc-rs v1.19.0
├── rustls v0.23.46
│   ├── rustls feature "aws-lc-rs"
│   │   └── rustls feature "aws_lc_rs"
│   │       └── rustls feature "prefer-post-quantum"
│   │           └── crypto-lab feature "pq" (command-line)
...

lib.rs 的 feature 说明对 prefer-post-quantum 只写了「This feature merely alters the order of rustls::crypto::aws_lc_rs::DEFAULT_KX_GROUPS」,没有提它会拉进 aws-lc-rs。Cargo feature 在整个依赖图里取并集,所以「两个都开」往往不是自己写的:一个依赖用默认 feature 的 rustls(默认就是 aws_lc_rs 加 prefer-post-quantum),另一个依赖开了 ring,合起来就是第四行。

📝NOTE
冷知识:这条 panic 消息在 0.23.30 换过一次

进程级默认值由 e7a1b418(「Introduce concept of "process default" provider」,首次进入 0.23.0)引入,提交说明写着默认值「can be set from the crate features, if and only if they are unambigious」。当时推断失败的消息只有一行:no process-level CryptoProvider available -- call CryptoProvider::install_default() before this point(0.23.0 发布前 88022fc9 刚把它从带反引号的写法改成这样,提交说明是「Avoid markdown and ensure it is a single sentence」)。这句话只让人去调 install_default,不说为什么推断不出来。8b8ba56e(「Clarify the ambiguous process-level CryptoProvider error」,首次进入 0.23.30)把它换成现在的三行,第二行点明「make sure exactly one of the 'aws-lc-rs' and 'ring' features is enabled」。0.23.0 到 0.23.29 打印的都是旧的那一句,按报错文本搜索时两句都要试。

修法:自己装一个

在 main 的开头、任何 builder() 之前装好,后面所有走默认值的地方都用它:

fn main() {
  rustls::crypto::ring::default_provider()
    .install_default()
    .expect("another provider was installed first");
  // ...
}

这样做之后第二次 install_default 会失败。实测先装 ring、再装 aws-lc-rs:

install ring:    true
install aws-lc:  Err(Arc), its secure_random: AwsLcRs
builder() uses the installed Arc: true
📝NOTE
没有蠢问题

问: install_default 返回 Result<(), Arc<Self>>,第二次失败时 Err 里的 Arc 是已经装好的那个 provider 吗?

答: 不是,是你这次传进去、没装上的那个。install_default 就是 OnceLock::set(Arc::new(provider)),set 失败时把参数原样还给调用方。上面的输出里 Err 带回来的 secure_random 是 AwsLcRs,而进程里生效的是 ring。要看生效的是谁,调 CryptoProvider::get_default()。

库和应用的分工写在 CryptoProvider 的文档里:库应该用 builder()(跟随应用装的默认值),应用应该在 main 里尽早 install_default。反过来,库如果自己 install_default,就会和应用抢这一次机会。

builder_with_provider

ClientConfig::builder_with_provider 完全绕开进程级默认值:

    pub fn builder_with_provider(
        provider: Arc<CryptoProvider>,
    ) -> ConfigBuilder<Self, WantsVersions> {
        ConfigBuilder {
            state: WantsVersions {},
            provider,
            time_provider: Arc::new(DefaultTimeProvider),
            side: PhantomData,
        }
    }

README 和 lib.rs 的 crate 文档都说可以「using the with_crypto_provider method on the respective config builder types」换 provider;0.23.46 里没有这个方法,仓库历史里也从没定义过它(git log -S'fn with_crypto_provider' 为空,这句话是 d9d08794 写文档时加进去的),实际的入口就是 builder_with_provider。它返回的状态是 WantsVersions,比 builder() 的 WantsVerifier 早一步:调用方要自己调 with_safe_default_protocol_versions() 或 with_protocol_versions(...),并处理它返回的 Result。builder() 其实就是 builder_with_provider(默认值).with_protocol_versions(DEFAULT_VERSIONS).unwrap(),那个 unwrap 是它的文档里说的第二种 panic。with_protocol_versions 在这里对 provider 做三项检查:

  1. 至少有一个套件属于所选的协议版本,否则 "no usable cipher suites configured";
  2. kx_groups 非空,否则 "no kx groups configured";
  3. 每个套件要求的密钥交换算法(ECDHE 或 DHE)至少有一个组能提供,否则报 Error::General,文本以「Ciphersuite ... requires ... key exchange, but no ...-compatible key exchange groups were present」开头。

只检查这些。两个列表里的东西来自哪个库、彼此是否搭配得当,rustls 不管:一个 provider 可以用 ring 的套件配 aws-lc-rs 的组。no_std 下没有 builder_with_provider(它要用 DefaultTimeProvider 读系统时间),对应的是 builder_with_details,再多传一个 TimeProvider。

证书校验器同理:with_root_certificates 用的是配置自己的 provider 的算法表,不会去碰进程级默认值;只有显式调用 WebPkiServerVerifier::builder(roots)(不带 _with_provider)才会。所以「全程 builder_with_provider、但中间用了一次 WebPkiServerVerifier::builder」的程序,在两个 provider 都开时照样 panic。

自定义 provider 要实现什么

文档给的最小例子是「只换一个字段」:

pub fn provider() -> rustls::crypto::CryptoProvider {
  rustls::crypto::CryptoProvider{
    key_provider: &HsmKeyLoader,
    ..aws_lc_rs::default_provider()
  }
}

结构体更新语法把其余字段留给内置 provider。完整的例子是工作区成员 provider-example,它用 RustCrypto 和 dalek 的 crate 从零拼了一个 provider,provider() 只填了一个 TLS 1.3 套件、一个 TLS 1.2 套件、一个密钥交换组。照着它数,要实现的东西是:

字段要实现的 traitprovider-example 里
cipher_suites构造 Tls13CipherSuite:hash::Hash 加 hash::Context;Hkdf,或者只实现 hmac::Hmac 再套 HkdfUsingHmac;Tls13AeadAlgorithm 产出 MessageEncrypter / MessageDecrypter;TLS 1.2 另要 Tls12AeadAlgorithm 和 Prf(或 PrfUsingHmac)Sha256、Sha256Hmac 套 HkdfUsingHmac、Chacha20Poly1305;quic: None
kx_groupsSupportedKxGroup 和 ActiveKeyExchangex25519-dalek 的 X25519
signature_verification_algorithms一张 WebPkiSupportedAlgorithms 表,元素是 pki-types 的 SignatureVerificationAlgorithm只有 RSA-PSS-SHA256 和 RSA-PKCS1-SHA256
secure_randomSecureRandom,一个 fillrand_core::OsRng
key_providerKeyProvider,返回 SigningKey,SigningKey 再给出 Signer只认 PKCS#8 的 ECDSA P-256

几个读例子时值得注意的地方:

  • 例子的 provider() 不是一个自洽的通用 provider:签名校验表只有 RSA,私钥只认 ECDSA P-256。它的 server 例子用 P-256 证书,client 例子连的是外网站点。用同一个 provider 的客户端去连它自己的服务端会怎样?实验把 ring provider 的校验表换成同样的两项 RSA 算法,去连一个 P-256 证书的服务端,ClientHello 里的 signature_algorithms 只剩两个 RSA 方案,服务端第一步就失败:
server: Some(PeerIncompatible(NoSignatureSchemesInCommon))
client: Some(AlertReceived(HandshakeFailure))
  • EcdsaSigningKeyP256::try_from 遇到非 PKCS#8 的私钥直接 panic!("unsupported private key format"),而不是返回错误;KeyProvider::load_private_key 的文档要求的是「Return an error if the key type encoding is not supported」。
  • 每个 trait 都有一个默认返回 false 的 fips(),自定义实现不覆盖它,CryptoProvider::fips() 就是假。这个方法把五个字段的 fips() 全部「与」起来(见套件与密钥交换的 FIPS 一节)。
  • 只需要换一部分时用结构体更新语法,CryptoProvider 的文档也这样建议:「uninteresting elements would be delegated back to one of the default providers」。

本仓库的 workspace 依赖(hpke-rs 等)不在本机的 cargo 缓存里,provider-example 没有编译运行,上面的内容来自读 tag 上的源码。为了看清 provider 的字段在一次握手里被调用的次数,实验写了一个最小的「自定义 provider」:ring 的默认 provider,只把 secure_random 和 key_provider 换成带计数的包装:

#[derive(Debug)]
struct CountingRandom { calls: AtomicUsize, bytes: AtomicUsize }

impl SecureRandom for CountingRandom {
  fn fill(&self, buf: &mut [u8]) -> Result<(), GetRandomFailed> {
    self.calls.fetch_add(1, Relaxed);
    self.bytes.fetch_add(buf.len(), Relaxed);
    println!("    fill({} bytes)", buf.len());
    ring::default_provider().secure_random.fill(buf)
  }
}

static CLIENT_RNG: CountingRandom = CountingRandom { calls: AtomicUsize::new(0), bytes: AtomicUsize::new(0) };

let client = Arc::new(CryptoProvider { secure_random: &CLIENT_RNG, ..ring::default_provider() });

secure_random 的类型是 &'static dyn SecureRandom,所以包装对象放在 static 里。构建服务端配置时,load_private_key called 打印了一次,之后的握手里再没出现。

❗IMPORTANT
动脑筋:一次完整的 TLS 1.3 握手(不恢复会话、不开 ECH),客户端的 secure_random.fill 被调用几次、一共要多少字节?服务端呢?

上面的程序,ring provider,ServerConfig 用 with_single_cert 的默认设置。答案在文末。

动脑筋答案

building configs:
    load_private_key called
handshake:
    fill(32 bytes)
    fill(2 bytes)
    fill(32 bytes)
    fill(32 bytes)
    fill(32 bytes)
    fill(4 bytes)
    fill(32 bytes)
    fill(32 bytes)
    fill(4 bytes)
    fill(32 bytes)
rounds [(236, 662), (80, 184)]
client secure_random: 3 calls, 66 bytes
server secure_random: 7 calls, 168 bytes

客户端 3 次 66 字节,全在 ClientHelloInput::new 里:32 字节的 legacy session id(TLS 1.3 的兼容模式要一个非空的随机 id),2 字节的 extension_order_seed(ClientHello 扩展顺序随机化的种子,c296594d「Randomize ClientHello extensions」引入),32 字节的 Random。

服务端 7 次 168 字节:32 字节的 ServerHello.random,然后每张会话票据 3 次。with_single_cert 的默认配置发 2 张票(send_tls13_tickets: 2),票据器是 NeverProducesTickets,所以走有状态的分支:emit_ticket 先取 32 字节的票据 nonce、4 字节的 ticket_age_add,再取 32 字节当会话缓存里的 id。32 + 2 × (32 + 4 + 32) = 168。

两边的 X25519 临时密钥都没有经过 secure_random:rustls 的 ring 密钥交换代码在 SupportedKxGroup::start 里直接 SystemRandom::new()。服务端 ECDSA 签名要的随机数也一样,crypto/ring/sign.rs 里签名前同样新建一个 SystemRandom。替换 secure_random 只替换了 rustls 协议层要的这几十个字节。

小结

  • CryptoProvider 是五个公开字段:两个有序列表(套件、组)、一张签名校验算法表、一个随机数源、一个私钥加载器;后两者和算法表只在构建配置时用,列表和随机数每次握手都用。
  • builder()、ServerConfig::builder()、WebPki*Verifier::builder() 走进程级默认值:先 get_default,没有就按 feature 推断并安装,推断不出就在 crypto/mod.rs:249 panic。
  • 推断成功的条件是恰好开了 ring 和 aws_lc_rs 之一、没开 custom-provider。prefer-post-quantum 隐含 aws_lc_rs,ring 加它就是两个都开。
  • install_default 只能成功一次;失败时 Err 带回的是本次传入的 provider。
  • builder_with_provider 绕开默认值,代价是自己选协议版本并处理三项一致性检查的错误。
  • 自定义 provider 用结构体更新语法替换需要的字段;provider-example 演示了全部 trait,但它的签名校验和私钥类型并不配套。

💬 关联讨论

发起讨论
加载中...