怎么读这一卷
主线位置:三条主线的总览。本页把握手、记录、校验三条主线各拆成几步,标出每一步落在哪个函数、哪一页讲它;再逐个核对
rustls/Cargo.toml里的 feature,其中prefer-post-quantum在默认 feature 里、又隐含aws_lc_rs,所以「默认 feature 加上ring」会让两个 provider 同时编进来,进程级默认 provider 就选不出来了;最后说明manual/模块怎么读。一次握手在本机上把前两条线跑一遍。
flowchart TB
start[从这里开始] --> q{读这一卷为了什么}
q -->|会用| use1["01 全景 再到 03"]
q -->|读懂实现| all[00 到 09 按顺序]
q -->|只关心握手| h["一次握手 再到 03"]
q -->|只关心证书| v["04 证书校验"]
锁定版本
本卷读三个仓库,各锁一个 tag。三个 tag 名里都带斜杠:
| 仓库 | tag | commit | 说明 |
|---|---|---|---|
| rustls | v/0.23.46 | f04804fa(2026-10-10,「Delay saving KX hint until handshake confirmation」) | crates.io 包 rustls-0.23.46 的 .cargo_vcs_info.json 记的就是它;包内路径是 rustls/ 子目录 |
| rustls-webpki | v/0.103.16 | 8ecdcc7(2026-10-09) | crates.io 包同样指向这个 commit |
| rustls-pki-types | v/1.15.1 | e29ad15(2026-07-22) | 同上 |
rustls 的 0.23.x 在 rel-0.23 分支上发版,git merge-base --is-ancestor v/0.23.46 origin/main 返回假:main 已经是 0.24 的开发线,两条线在 4e0b5fed(0.23.29)分开。所以「这段代码在 tag 之后改过没有」要用 git diff v/0.23.46 origin/main -- <文件> 看,git log v/0.23.46..origin/main 在这里没有意义。
源码链接一律指向 tag:https://github.com/rustls/rustls/blob/v/0.23.46/<路径>#L<行>,行号是条目开始的那一行。本地仓库的工作区在 main 上,读代码要用 git show "v/0.23.46:rustls/src/<文件>"。zsh 里把 tag 放进变量时写 "${T}:rustls/...",写成 "$T:rustls/..." 会被 zsh 当成 :r 修饰符,静悄悄地取错路径。
三条主线
握手主线:一次 TLS 1.3 完整握手
flowchart TB new["ClientConnection new"] --> ch["发 ClientHello"] ch --> sch["ExpectClientHello"] sch --> sflight["ServerHello 加密飞行"] sflight --> csh["ExpectServerHello"] csh --> cee["EE 到 Finished"] cee --> cfin["客户端 Finished"] cfin --> sfin["服务端 ExpectFinished"] sfin --> tick["两张票据"]
| 步骤 | 源码 | 讲它的页 |
|---|---|---|
1. ClientConnection::new → start_handshake → emit_client_hello_for_retry:选初始密钥共享(initial_key_share),拼扩展,下一个状态是 ExpectServerHelloOrHelloRetryRequest | client/hs.rs | 一次握手、客户端 TLS 1.3 |
2. 服务端 ExpectClientHello 选版本、套件、组、证书,交给 CompleteClientHelloHandling::handle_client_hello:emit_server_hello、假 CCS、emit_encrypted_extensions、证书、emit_certificate_verify_tls13、emit_finished_tls13 | server/hs.rs、server/tls13.rs | 服务端 TLS 1.3 |
3. 客户端 handle_server_hello 算出握手密钥,然后依次是 ExpectEncryptedExtensions → ExpectCertificateOrCertReq → ExpectCertificate → ExpectCertificateVerify → ExpectFinished | client/tls13.rs | 状态机、客户端 TLS 1.3 |
4. 每一步推进密钥日程:KeyScheduleEarly → KeyScheduleHandshakeStart → … → KeyScheduleTraffic,转录哈希在 HandshakeHash | tls13/key_schedule.rs、hash_hs.rs | 密钥日程 |
5. 客户端发 Finished,进入 ExpectTraffic;服务端 ExpectFinished 校验后用 emit_ticket 发票据(默认 send_tls13_tickets = 2),进入 ExpectTraffic | client/tls13.rs、server/tls13.rs | 会话恢复与 0-RTT |
记录主线:一段应用数据的往返
| 步骤 | 源码 | 讲它的页 |
|---|---|---|
1. writer() 返回的 Writer 把明文交给连接;握手没完成时先缓冲在 sendable_plaintext | conn.rs、common_state.rs | 发送路径 |
2. send_appdata_encrypt 按 MessageFragmenter 切片,send_single_fragment 调 RecordLayer::encrypt_outgoing | msgs/fragmenter.rs、record_layer.rs | 加密与序列号、发送路径 |
3. 密文进 ChunkVecBuffer,write_tls 把它写给调用方的 io::Write | vecbuf.rs、conn.rs | sans-IO、发送路径 |
4. 对端 read_tls 把字节读进 DeframerVecBuffer | msgs/deframer/ | 去帧与缓冲 |
5. process_new_packets → DeframerIter 按 5 字节头切记录 → RecordLayer::decrypt_incoming → process_msg 交给当前状态 | conn.rs、record_layer.rs | 去帧与缓冲、加密与序列号 |
6. reader() 读出明文;没有明文时返回 WouldBlock | conn.rs | sans-IO |
校验主线:一张服务器证书怎么被信任
| 步骤 | 源码 | 讲它的页 |
|---|---|---|
1. 客户端 ExpectCertificateVerify 里调配置的校验器;默认是 WebPkiServerVerifier,根证书来自 RootCertStore | client/tls13.rs、webpki/ | 服务器证书 |
2. verify_server_cert → verify_server_cert_signed_by_trust_anchor → rustls-webpki 的 EndEntityCert::verify_for_usage | webpki/verify.rs | 服务器证书 |
3. 路径构建:ChainOptions::build_chain,签名检查次数受 Budget 限制 | webpki src/verify_cert.rs | 路径构建 |
4. 名字匹配:verify_server_name → verify_is_valid_for_subject_name | webpki src/subject_name/ | 服务器证书 |
5. CertificateVerify 的签名:verify_tls13_signature | webpki/verify.rs | 服务器证书 |
6. 吊销(可选):CRL,RevocationOptions | webpki src/crl/ | 客户端证书与自定义校验 |
三条线在 ConnectionCore 交汇:记录主线把字节变成一条条消息,交给握手主线的当前状态;握手主线在 Certificate 和 CertificateVerify 两步上调用校验主线。
实验环境
| 项 | 本卷 |
|---|---|
| 平台 | M2 Max、macOS 27、rustc 1.99.0,aarch64;计时用 release,取多次里的最小值并注明负载(写作时有其他实验并行在跑,uptime 负载在 4 到 9 之间) |
| rustls | crates.io 上的 =0.23.46,打开 ring、aws_lc_rs、logging(加上默认 feature);另有 rustls-pki-types 1、webpki-roots 1、rcgen 0.14(造测试证书)、tokio-rustls 0.26 |
| 默认握手 | 进程内存里一个客户端、一个服务端,ring provider,rcgen 生成的 localhost 自签名证书(ECDSA P-256),客户端只信任这一张证书 |
| OpenSSL | Homebrew 的 openssl 命令,OpenSSL 4.1.0 23 Oct 2026;/usr/bin/openssl 是 macOS 自带的 LibreSSL,本卷不用 |
| 网络 | 不连外网,所有握手都在内存或 127.0.0.1 上 |
会随运行变化的数字页面上会注明:服务端第一轮的字节数在 662 到 664 之间浮动(证书和 CertificateVerify 里的 ECDSA 签名是 DER 编码,长度差一两个字节);随机数、密钥、密文每次都不同。
feature 各打开什么
rustls/Cargo.toml 的 [features],默认是 aws_lc_rs、logging、prefer-post-quantum、std、tls12:
| feature | 打开什么 | 关掉或打开时的变化 |
|---|---|---|
aws_lc_rs(默认,别名 aws-lc-rs) | 依赖 aws-lc-rs(带 aws-lc-sys 和 prebuilt-nasm),并打开 webpki/aws-lc-rs | 编进 crypto::aws_lc_rs provider;aws-lc-sys 要编 C 代码 |
ring | 依赖 ring,并打开 webpki/ring | 编进 crypto::ring provider |
prefer-post-quantum(默认) | = ["aws_lc_rs"] | 只改 aws_lc_rs::DEFAULT_KX_GROUPS 的顺序,把 X25519MLKEM768 排第一;对 ring provider 没有作用,却会把 aws-lc-rs 拉进依赖 |
fips | aws_lc_rs 加 aws-lc-rs?/fips 和 webpki/aws-lc-rs-fips | 配置构建器里 require_ems 的默认值取 provider.fips(),FIPS provider 下要求扩展主密钥 |
custom-provider | 空 feature | from_crate_features 不再按 feature 选内置 provider,必须自己 install_default |
std(默认) | 打开 webpki/std、pki-types/std、once_cell/std | 关掉后没有 Connection、Stream、KeyLogFile,只剩 unbuffered 接口;crate 本身是 #![no_std] 加 alloc |
tls12(默认) | 空 feature,按 cfg 编进 tls12 模块、version::TLS12 和 TLS 1.2 的套件 | 文档提醒:feature 是叠加的,依赖图里别人可能又把它打开,要禁 TLS 1.2 应该在配置里只选 TLS 1.3 |
logging(默认) | 依赖 log | 关掉后 trace! / debug! 等变成空宏(lib.rs 里的 mod log) |
brotli / zlib | 依赖 brotli 或 zlib-rs | RFC 8879 证书压缩 |
read_buf | 构建依赖 rustversion,加 std | build.rs 只在 nightly 上设 cfg(read_buf),稳定版上什么都不做 |
hashbrown 不在 [features] 里,但它是可选依赖,自动生成同名 feature;lib.rs 在没有 std 时用它提供 HashMap。
feature 之间互相牵连的地方:
- 两个 provider 同时在,就没有进程级默认值。
from_crate_features只在「恰好开了一个、且没开custom-provider」时返回 provider。本卷的实验 crate 两个都开,直接调ClientConfig::builder()(release):
get_default before: false
thread 'main' (7435046) 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.
- 「我只要 ring」要先关默认 feature。
prefer-post-quantum在默认 feature 里且隐含aws_lc_rs,所以rustls = { features = ["ring"] }编进来的是两个 provider,结果就是上面这条 panic。crates.io 上 sqlx-core 0.8.6 和 lettre 0.11.23 依赖 rustls 时都写了default-features = false,provider 由它们自己的 feature 再打开;本站选的是哪个,见 08 在本站中。 fips和prefer-post-quantum都会悄悄打开aws_lc_rs。 在一个 crate 里开fips,等于也开了 aws-lc-rs。
provider 怎么选、怎么装成默认值,见 CryptoProvider。
manual 模块当资料读
rustls/src/manual/ 是编进 rustdoc 的设计说明,六节:implvulns.rs(别家实现的漏洞和 rustls 怎么避开)、tlsvulns.rs(协议层面的漏洞:CBC、BEAST、CRIME、Logjam、DROWN、Poodle、重协商、3SHAKE、KCI 等)、howto.rs(UnexpectedEof 的含义、怎么开日志、怎么用 SSLKEYLOGFILE 抓包)、features.rs(支持和不支持的协议特性)、defaults.rs(默认值的理由)、fips.rs。仓库根目录还有 README.md、ROADMAP.md、BENCHMARKING.md、examples/。
本卷把它们当资料读,引用前在源码或实验里核实。它们和 tag 上的代码有几处对不上:
implvulns.rs用一串 TLS 1.2 客户端状态说明状态机,写的是tls12::ExpectServerKX、tls12::ExpectCCS;代码里是ExpectServerKx、ExpectCcs,而且ExpectCertificate之后先进的是ExpectCertificateStatusOrServerKx,清单里没有这一步。- README 和
lib.rs说定制 provider「using thewith_crypto_providermethod on the respective config builder types」,0.23.46 里没有这个方法,入口是ClientConfig::builder_with_provider/ServerConfig::builder_with_provider。 lib.rs的文档里[write_tls()]: Connection::read_tls,write_tls的链接指向了read_tls。features.rs里「跳过所有认证」的示例链接指向v/0.23.23的tlsclient-mio.rs,不是当前版本。
不讲什么
- bogo(BoringSSL 的测试套件适配层)、
openssl-tests、fuzz、ci-bench的细节:源码地图 里各一句。 - FIPS 认证本身:只讲
fipsfeature 改了什么,见 套件与密钥交换。 rustls-post-quantumcrate(0.2.2,262 行):0.23.x 里X25519MLKEM768已经在 aws-lc-rs provider 里,本卷不再讲它。ring和aws-lc-rs的内部实现。
阅读路线
- 只想会用:01 全景 的四页;CryptoProvider;服务器证书;07 常见坑。
- 想读懂实现:按顺序读。02 讲清楚字节怎么变成消息,03 的状态机才好读;04 和 05 是 03 在两个点上调出去的东西。
- 只关心握手:一次握手 → 状态机 → 客户端 TLS 1.3 → 服务端 TLS 1.3 → 密钥日程。
- 只关心证书:服务器证书 → 路径构建 → 客户端证书与自定义校验。
前置知识
- TLS 1.3 的握手:ClientHello 带着密钥共享(key share),服务端回 ServerHello 后,其余握手消息都已加密;RFC 8446 是唯一的权威,本卷引用节号。
- 记录层(record layer):每条记录 5 字节头(类型、版本、长度)加最多 2^14 字节的内容,TLS 1.3 里加密记录的外层类型一律写成应用数据。
- 密钥日程(key schedule):HKDF 从共享秘密一步步派生出握手密钥和应用密钥。
- Rust:trait 对象、
Box<Self>接收者、std::io::{Read, Write}。读过 hyper 卷的 IO trait 会帮助理解「库不自己做 IO」的设计。
小结
- 三个 tag:rustls
v/0.23.46(f04804fa)、webpkiv/0.103.16、pki-typesv/1.15.1,都和 crates.io 包一致;0.23.x 在rel-0.23分支上,不是 main 的祖先。 - 握手主线:
ClientConnection::new→ExpectClientHello→ 服务端一次发出 ServerHello 和加密飞行 → 客户端五个Expect*→ Finished → 票据。 - 记录主线:
writer→ 分片 →encrypt_outgoing→write_tls→read_tls→ 去帧 →decrypt_incoming→reader。 - 校验主线:
WebPkiServerVerifier→ webpki 路径构建(有预算)→ 名字匹配 → CertificateVerify 签名 → 可选的 CRL。 - 默认 feature 含
prefer-post-quantum,它隐含aws_lc_rs;只想用 ring 必须关默认 feature,否则两个 provider 都在,进程级默认值选不出来,panic 在crypto/mod.rs:249。 manual/和 README 是资料:TLS 1.2 状态名、with_crypto_provider、write_tls的文档链接和代码对不上。