rustls 解决什么问题
主线位置:握手主线的起点之前。本页不跟某一条消息,而是回答「为什么要有这个库」:它承诺什么、拒绝做什么,这些拒绝落在哪几行代码上;再把同一个握手放在 ring 和 aws-lc-rs 两个 provider 上量耗时和二进制大小,最后说明和 OpenSSL 在本机上能做的对照,以及它为什么只能回答一部分问题。
flowchart TB goal["安全的默认值"] --> no["不实现旧协议"] goal --> safe["forbid unsafe"] goal --> sans["不做 IO"] no --> v["只有 TLS12 和 TLS13"] no --> s["九个 AEAD 套件"] no --> r["拒绝重协商"] goal --> prov["CryptoProvider"] prov --> ring["ring"] prov --> aws["aws-lc-rs 默认"]
定位
README 和 lib.rs 开头是同一段话:
Rustls is a TLS library that aims to provide a good level of cryptographic security, requires no configuration to achieve that security, and provides no unsafe features or obsolete cryptography by default.
这句话在代码里落成三件事:
- 不用配置就安全。
ClientConfig::builder()的默认值就是能用的配置:协议版本是DEFAULT_VERSIONS(TLS 1.3 和 TLS 1.2),套件和密钥交换组来自 provider 的默认表,证书一定校验。关掉证书校验要走dangerous()这个名字本身就在警告的入口,见 客户端证书与自定义校验。 - 没有不安全的特性。 「不安全」有两层意思。内存安全上,
lib.rs第 332 行是#![forbid(unsafe_code, unused_must_use)],整个 crate 一行unsafe都没有,需要unsafe的只有密码学后端。协议上,见下一节。 - 不做 IO。 连接对象不开套接字、不查 DNS、不读文件,见 sans-IO。
lib.rs的原话是「It doesn't make or accept TCP connections, or do DNS, or read or write files」。
rustls 自己说自己是「a low-level library」,要发 HTTPS 请求的话建议用建在它上面的 hyper、ureq;用 tokio 的话用 tokio-rustls。
不实现的东西
manual/features.rs 的「Non-features」列了一张清单,开头一句是「rustls does not and will not support」。逐条到代码里核实:
| 清单里的条目 | 代码里怎么落实 |
|---|---|
| SSL1、SSL2、SSL3、TLS1、TLS1.1 | ALL_VERSIONS 只有 TLS13 和(tls12 feature 下的)TLS12,没有别的 SupportedProtocolVersion 可选 |
| RC4、DES / 3DES、EXPORT、MAC-then-encrypt、没有前向保密的套件 | ring provider 的 ALL_CIPHER_SUITES 一共九个:TLS 1.3 的三个,加 TLS 1.2 的六个 TLS_ECDHE_{ECDSA,RSA}_WITH_{AES_256_GCM,AES_128_GCM,CHACHA20_POLY1305},全是 ECDHE 加 AEAD |
| 重协商 | TLS 1.2 握手完成后,process_main_protocol 收到 HelloRequest(客户端)或 ClientHello(服务端),回一个 NoRenegotiation 警告告警就继续,次数还受 temper_counters 限制 |
离散对数 Diffie-Hellman(带 *) | 内置 provider 的 ALL_KX_GROUPS 只有 X25519、P-256、P-384(aws-lc-rs 另有后量子混合组);公开的 ffdhe_groups 模块只提供 RFC 7919 的群参数,内置 provider 不实现 FFDHE 密钥交换 |
| 把 CA 证书直接当服务器证书(自签名 CA) | 见下面的实验 |
清单里带 * 的条目,意思是「可以通过公开 API(例如 CryptoProvider)扩展」,也就是默认不做、但不拦着自定义 provider。manual/tlsvulns.rs 逐个讲了 CBC、BEAST、CRIME、Logjam / FREAK、SWEET32、DROWN、Poodle、GCM nonce、重协商、3SHAKE、KCI 这些历史漏洞和 rustls 的对策,本卷不重复。
最后一条最容易误解:「自签名证书不支持」不是说自签名证书不能用。用 OpenSSL 造两张 localhost 的自签名 ECDSA P-256 证书,一张 basicConstraints=CA:TRUE(openssl req -x509 的默认),一张 CA:FALSE,都放进客户端的 RootCertStore,再拿同一张去当服务器证书,内存里握手(ring,release):
CA:TRUE cert as its own leaf: Err(InvalidCertificate(Other(OtherError(CaUsedAsEndEntity))))
CA:FALSE self-signed leaf: Ok("TLSv1_3 TLS13_AES_256_GCM_SHA384 X25519")
不支持的是「一张 CA 证书同时当信任锚和终端证书」。features.rs 给的理由是「in order to limit complexity and risk in path building」。错误来自 rustls-webpki 的路径构建,见 路径构建。本卷的实验证书(rcgen 默认或 OpenSSL 加 CA:FALSE)都是终端证书,所以能直接放进根证书库。
两个 provider:耗时
同一个握手(ring 或 aws-lc-rs,同一张 ECDSA P-256 证书,客户端关掉会话恢复,每次都是完整握手),在进程内存里连做 50 次热身,再做 5000 次,取单次握手(含建两个连接对象、两轮收发、双方所有密码学运算)的最小值和中位数(release,uptime 负载 6 到 8):
ring n=5000 min= 222.3us median= 233.6us (TLSv1_3 TLS13_AES_256_GCM_SHA384 X25519)
aws n=5000 min= 249.3us median= 266.0us (TLSv1_3 TLS13_AES_256_GCM_SHA384 X25519MLKEM768)
aws-x25519 n=5000 min= 210.8us median= 224.1us (TLSv1_3 TLS13_AES_256_GCM_SHA384 X25519)
又跑了两次,最小值分别是 222.4 / 245.9 / 209.2 µs 和 217.5 / 248.8 / 211.6 µs。读法:
aws是aws_lc_rs::default_provider()原样。实验 crate 没关默认 feature,prefer-post-quantum在,DEFAULT_KX_GROUPS第一位是X25519MLKEM768,协商结果也就是它。它比 ring 慢约 25 µs,多出来的是 ML-KEM-768 的密钥生成、封装、解封装。aws-x25519把kx_groups换成只有 X25519,和 ring 比的是同一套算法。这时 aws-lc-rs 比 ring 快约 10 µs(5%)。- 一次完整握手两百多微秒,客户端和服务端各占一部分;在 127.0.0.1 上走 TCP 时,单次握手的最小值是 570 到 720 µs(见下文),网络栈的开销比密码学还大。
7415b5ff(2024-02-09,「Change crate default provider to aws-lc-rs」)把 rustls/Cargo.toml 的 default = ["logging", "ring", "tls12"] 改成了 default = ["aws_lc_rs", "logging", "tls12"],首次发布于 0.23.0。同一个提交改了 lib.rs 的文档:之前写的是「by default it uses [ring] ... As a result, rustls only runs on platforms supported by ring」,之后是「by default it uses [aws-lc-rs]」,并改为让读者去看 aws-lc-rs 的平台支持说明。提交说明只有标题,没有写理由。一年后 1b64a92c(2025-04-11,首次发布于 0.23.27)又把 prefer-post-quantum 加进默认 feature,提交说明是「This enables, for dependents who use default-features, X25519MLKEM768 as the most-preferred key exchange」。上面 aws 那一行协商出 X25519MLKEM768,就是这两个提交叠加的结果。
两个 provider:二进制大小
三个最小程序,都是 release、默认 profile(debug = 0),macOS 上用 strip 剥掉符号:
| 程序 | 未剥离 | 剥离后 | 减去空程序 |
|---|---|---|---|
println!("hello"),不依赖 rustls | 435088 | 342792 | — |
rustls(default-features = false,ring、std、tls12),内存里握手一次 | 2030288 | 1643160 | 约 1.30 MB |
同上,把 ring 换成 aws_lc_rs | 3542144 | 3070168 | 约 2.73 MB |
两个程序除 provider 外完全相同,证书和私钥用 include_bytes! 嵌进去(不依赖 rcgen,免得把 rcgen 的后端也算进来)。otool -L 显示两者都只动态链接系统库:aws-lc-rs 的 C 代码(aws-lc-sys 0.47.0)是静态链接进来的。aws-lc-rs provider 里有 ring provider 没有的东西:ML-KEM 和后量子混合组(crypto/aws_lc_rs/pq/)、HPKE(hpke.rs,ECH 要用),即使不用也会编进来一部分。
作为参照,Homebrew 的 OpenSSL 4.1.0 是动态库:libssl.4.dylib 996160 字节,libcrypto.4.dylib 4939376 字节,openssl 命令本身 920352 字节。它们是整套库,包含 rustls 不实现的协议和算法,不能和上表直接相减比较。
和 OpenSSL 对照:能比什么
本机的 openssl 是 Homebrew 的 OpenSSL 4.1.0 23 Oct 2026(/usr/bin/openssl 是 LibreSSL,不用)。没有 C 程序可以链接 libssl 做「库对库」的对照,只能用它自带的 s_server 和 s_time。为了让条件一致:
- 同一张 ECDSA P-256 证书和私钥,TLS 1.3,套件固定
TLS_AES_256_GCM_SHA384,密钥交换固定 X25519(s_server -groups X25519;s_time没有-groups,用OPENSSL_CONF里的Groups = X25519让它只发一个 X25519 密钥共享); - 都是完整握手、都校验证书(
s_time -verify 1 -CAfile;rustls 本来就校验),都不复用会话;服务端都发两张票据(rustls 的send_tls13_tickets默认 2;s_client -msg实测s_server也发 2 张); - 一个客户端顺序建连接,每次握手完就关,跑 10 秒,在
127.0.0.1上做四种组合。
吞吐(每秒握手数,墙钟时间) 两次运行(uptime 负载 4 到 6),完整输出在实验目录:
| 客户端 → 服务端 | 第 1 次 | 第 2 次 |
|---|---|---|
openssl s_time → openssl s_server | 908/s | 870/s |
openssl s_time → rustls(ring) | 932/s | 896/s |
rustls(ring)→ openssl s_server | 1045/s | 1023/s |
| rustls(ring)→ rustls(ring) | 1050/s | 1055/s |
| rustls(aws-lc-rs, X25519)→ rustls(aws-lc-rs, X25519) | 1091/s | 1081/s |
所有组合都在每秒 900 到 1100 次之间,单次 1 毫秒左右,是内存握手的四五倍。在本机回环上,瓶颈是建 TCP 连接、两个进程之间来回唤醒,而不是 TLS 库:换哪一端,数字只动 10% 上下。
CPU(每次握手的 user + sys) 固定一端换另一端,用 /usr/bin/time -p 量被换那一端进程的总 CPU 时间,除以握手次数,各跑 10 秒、三次(数字取第三次,另两次相差不到 5%):
== server CPU, client = openssl s_time
server openssl: 8994 handshakes, user 1.71s sys 0.56s -> 252us CPU/handshake
server ring: 10104 handshakes, user 0.92s sys 0.60s -> 150us CPU/handshake
server aws-x25519: 10242 handshakes, user 0.90s sys 0.71s -> 157us CPU/handshake
== client CPU, server = openssl s_server
client openssl s_time: 10138 handshakes, user 2.31s sys 0.90s -> 317us CPU/handshake
client rustls ring: 10484 handshakes, user 1.72s sys 0.77s -> 238us CPU/handshake
client rustls aws-x25519: 10618 handshakes, user 1.62s sys 0.82s -> 230us CPU/handshake
这组数字只能读到「同一数量级,rustls 这一端用的 CPU 更少」,不能读成「rustls 比 OpenSSL 快 40%」:
s_server和s_time是 OpenSSL 的诊断工具,不是一个精简的服务端或客户端。s_server -www在握手后还要等 HTTP 请求、处理连接被对端关掉;本卷的 rustls 服务端握手完、发完票据就关。两边每次握手做的额外工作不一样。- sys 时间(TCP 建连、收发)两边接近,差别主要在 user 时间。一个只链接 libssl、和 rustls 程序做完全相同的事的 C 程序才能把差别归到库上,本机实验没有做这一步。
所以本卷对「rustls 和 OpenSSL 谁快」的回答是:在回环上做完整握手,两者的吞吐被网络栈拉平;CPU 上 rustls 的程序更省,但对照对象是诊断工具,差距的大小不可信。
小结
- 定位:不用配置就安全、没有不安全的特性、不做 IO;整个 crate
forbid(unsafe_code)。 - 不支持的东西在代码里是「根本没有」:只有 TLS 1.2 / 1.3 两个版本,九个 ECDHE 加 AEAD 套件,重协商请求只回一个
NoRenegotiation警告。 - 自签名证书能用,只要它不是 CA 证书;
CA:TRUE的证书当叶子会得到CaUsedAsEndEntity。 - 内存握手(含双方):ring 约 220 µs,aws-lc-rs 默认(X25519MLKEM768)约 250 µs,aws-lc-rs 只用 X25519 约 210 µs。
- 剥离后的最小程序:ring 1.64 MB,aws-lc-rs 3.07 MB,空程序 0.34 MB。
- 和 OpenSSL 在回环上的吞吐都是每秒约一千次,被 TCP 拉平;CPU 上的差距对照的是
s_server/s_time这类诊断工具,不能当库的结论。