Rust in Everything
基于 rustls 0.23.46

服务端 TLS 1.3

主线位置:握手主线的服务端一侧。ClientHello 到达后,ExpectClientHello 在一次 handle 里做完全部决定(版本、证书、套件、密钥交换组、要不要恢复会话),并把 ServerHello 到 Finished 的整批消息排进发送队列;之后只等客户端的 Finished,验过以后发会话票据,进入 ExpectTraffic。

stateDiagram-v2
  [*] --> ExpectClientHello
  ExpectClientHello --> ExpectClientHello: 发 HRR
  ExpectClientHello --> ExpectAndSkipRejectedEarlyData: HRR 且带 early_data
  ExpectAndSkipRejectedEarlyData --> ExpectClientHello
  ExpectClientHello --> ExpectFinished: 常见路径
  ExpectClientHello --> ExpectEarlyData: 接受 0-RTT
  ExpectEarlyData --> ExpectFinished: EndOfEarlyData
  ExpectClientHello --> ExpectCertificate: 要客户端证书
  ExpectCertificate --> ExpectCertificateVerify
  ExpectCertificate --> ExpectFinished: 可选且没给
  ExpectCertificateVerify --> ExpectFinished
  ExpectFinished --> ExpectTraffic
  ExpectFinished --> ExpectQuicTraffic: QUIC

服务端的代码在 rustls/src/server/ 下:hs.rs 785 行,放 TLS 1.2 和 1.3 共用的 ClientHello 处理;tls13.rs 1604 行,放 TLS 1.3 的其余状态;server_conn.rs 1298 行,放 ServerConfig、ServerConnection、Acceptor 和几个 trait。状态机本身怎么驱动(State trait、Box<dyn State>、出错后的告警)见状态机,这里只讲服务端每一步做了什么决定。

实验平台:M2 Max、macOS 27、rustc 1.99.0,rustls 0.23.46,ring provider(显式 builder_with_provider),release 构建,客户端和服务端都在同一进程的内存里,write_tls 的输出直接交给对端的 read_tls。程序在 exp-rustls/hsb-lab/src/bin/ 下。

起点:ExpectClientHello

ServerConnection::new 调 ConnectionCore::for_server,第一个状态是 ExpectClientHello:

pub(super) struct ExpectClientHello {
    pub(super) config: Arc<ServerConfig>,
    pub(super) extra_exts: ServerExtensionsInput<'static>,
    pub(super) transcript: HandshakeHashOrBuffer,
    #[cfg(feature = "tls12")]
    pub(super) session_id: SessionId,
    #[cfg(feature = "tls12")]
    pub(super) using_ems: bool,
    pub(super) previous_hello: Option<PreviousClientHello>,
    pub(super) send_tickets: usize,
}
  • transcript 开始时是缓冲(HandshakeHashBuffer),因为还不知道套件,也就不知道用哪个哈希;选定套件后才变成增量哈希。转录哈希的细节见密钥日程。
  • extra_exts 只有 QUIC 用,放 QUIC 传输参数。
  • previous_hello 是 None 表示这是第一个 ClientHello;发过 HelloRetryRequest 之后,下一个 ExpectClientHello 带着 Some,记住第一次的套件和有没有提供 PSK。

它的 handle 只有两行:

        let (client_hello, sig_schemes) =
            process_client_hello(&m, self.previous_hello.is_some(), cx)?;
        self.with_certified_key(sig_schemes, client_hello, &m, cx)

拆成两步是为了 Acceptor:第一步不需要 ServerConfig,第二步需要。Acceptor 在两步之间把控制权交还给调用方,后面单独讲。

不看配置的检查:process_client_hello

process_client_hello 做的都是与配置无关的检查:

  • 压缩方法里必须有 Null,否则发 illegal_parameter。
  • check_aligned_handshake:ClientHello 之后,同一批记录里不能再跟别的握手消息。
  • SNI:
    let sni = match &client_hello.server_name {
        Some(ServerNamePayload::SingleDnsName(dns_name)) => Some(dns_name.to_lowercase_owned()),
        Some(ServerNamePayload::IpAddress) => None,
        Some(ServerNamePayload::Invalid) => {
            return Err(cx.common.send_fatal_alert(
                AlertDescription::IllegalParameter,
                PeerMisbehaved::ServerNameMustContainOneHostName,
            ));
        }
        None => None,
    };

名字先转小写再存进 cx.data.sni,证书选择器拿到的总是小写。RFC 6066 不允许在 host_name 里放 IP 字面量,有的客户端偏要放;rustls 解析成 IpAddress,然后当作客户端没发 SNI。发过 HelloRetryRequest 之后,第二个 ClientHello 的 SNI 必须和第一个相同,否则是 ServerNameDifferedOnRetry。

  • 没有 signature_algorithms 扩展就发 handshake_failure:服务端需要它来挑证书和签名算法。

选版本

with_certified_key 第一件事是定版本:

        let tls12_enabled = self
            .config
            .supports_version(ProtocolVersion::TLSv1_2, cx.common.protocol)
            && !self.previous_hello.is_some();

        // Are we doing TLS1.3?
        let version = if let Some(versions) = &client_hello.supported_versions {
            if versions.tls13 && tls13_enabled {
                ProtocolVersion::TLSv1_3
            } else if !versions.tls12 || !tls12_enabled {
                return Err(cx.common.send_fatal_alert(
                    AlertDescription::ProtocolVersion,
                    PeerIncompatible::Tls12NotOfferedOrEnabled,
                ));
            // ...
  • 客户端发了 supported_versions 扩展:双方都支持 1.3 就是 1.3;否则退到 1.2,1.2 也不行就发 protocol_version 告警。
  • 没发这个扩展的是老客户端,按 legacy_version 判断,低于 1.2 直接拒绝。rustls 不实现 1.0 / 1.1。
  • supports_version 不只看 ServerConfig 里开了哪些版本,还看 provider 里有没有这个版本的套件、协议是不是 QUIC(QUIC 只能 1.3)。
  • 发过 HelloRetryRequest 之后 tls12_enabled 恒为假:HelloRetryRequest 本身就是 1.3 的消息,第二个 ClientHello 不能再退回 1.2。

一个只开 TLS 1.3 的 rustls 客户端连只开 TLS 1.2 的 rustls 服务端,两边的错误:

TLS 1.3-only client: server Err(PeerIncompatible(Tls12NotOfferedOrEnabled)); sent Alert 2; client Err(AlertReceived(ProtocolVersion))

选证书:ResolvesServerCert

版本定了,接下来选证书,再选套件。顺序是这样的原因在 TLS 1.2:1.2 的套件名里写着签名算法(ECDHE_ECDSA / ECDHE_RSA),套件能不能用取决于证书的密钥类型,所以先有证书。TLS 1.3 的套件和签名算法无关,usable_for_signature_algorithm 对 1.3 套件直接返回 true。

选证书之前,先把客户端的 signature_algorithms 按双方共有的套件过滤一遍(compatible_sigscheme_for_suites),然后拼出一个 ClientHello 交给 config.cert_resolver:

pub struct ClientHello<'a> {
    pub(super) server_name: &'a Option<DnsName<'a>>,
    pub(super) signature_schemes: &'a [SignatureScheme],
    pub(super) alpn: Option<&'a Vec<ProtocolName>>,
    pub(super) server_cert_types: Option<&'a [CertificateType]>,
    pub(super) client_cert_types: Option<&'a [CertificateType]>,
    pub(super) cipher_suites: &'a [CipherSuite],
    // ...
    pub(super) certificate_authorities: Option<&'a [DistinguishedName]>,
    pub(super) named_groups: Option<&'a [NamedGroup]>,
}

全是借用,不拷贝。certificate_authorities 在 TLS 1.2 下总是 None:代码注释说这是遵守 TLS 1.2 RFC。

ResolvesServerCert 只有一个必须实现的方法:

pub trait ResolvesServerCert: Debug + Send + Sync {
    fn resolve(&self, client_hello: ClientHello<'_>) -> Option<Arc<sign::CertifiedKey>>;
    // ...
}

返回 None 时服务端发 access_denied 告警,错误是 General("no server certificate chain resolved")。rustls 自带两个实现:

  • with_single_cert 用的 SingleCertAndKey,不管客户端说什么都给同一张证书。
  • ResolvesServerCertUsingSni,一个 HashMap<String, Arc<CertifiedKey>>。add 时用 webpki 检查证书确实对这个名字有效(注释说这只是帮服务端发现配置错误,与安全无关);resolve 按 SNI 查表,客户端没发 SNI 就返回 None。

两张自签证书(a.test、b.test)装进 ResolvesServerCertUsingSni,客户端分别连 b.test 和 c.test,再故意把 a.test 的证书登记成 c.test:

SNI b.test: server Ok(()); sent Handshake 122 [ServerHello/HRR] | ChangeCipherSpec 1 | ApplicationData 523; client Ok(())
SNI c.test: server Err(General("no server certificate chain resolved")); sent Alert 2; client Err(AlertReceived(AccessDenied))
add("c.test", cert for a.test) -> Err(InvalidCertificate(NotValidForNameContext { expected: DnsName("c.test"), presented: ["DnsName(\"a.test\")"] }))

客户端只看到一个 AccessDenied 告警,看不出是名字不对。排查时要看服务端日志。

resolve 是同步调用,在握手状态机内部执行。要查数据库、远程取证书这类需要 IO 的情况,trait 的文档指向 Acceptor。

选套件和组

choose_suite_and_kx_group 分三步:

  1. 遍历客户端的 supported_groups,按顺序为每个组在服务端 provider 里找对应的 SupportedKxGroup,记下 ECDHE / FFDHE 是否可能。都不可能就是 NoKxGroupsInCommon。
  2. 从服务端 provider 的套件里筛出可用的(版本对、协议对、密钥交换算法对、TLS 1.2 下签名算法对),再和客户端的套件列表求交:
        let suite = if self.config.ignore_client_order {
            suitable_suites_iter.find(|suite| client_suites.contains(&suite.suite()))
        } else {
            let suitable_suites = suitable_suites_iter.collect::<Vec<_>>();
            client_suites
                .iter()
                .find_map(|client_suite| {
                    suitable_suites
                        .iter()
                        .find(|x| *client_suite == x.suite())
                })
                .copied()
        }
        .ok_or(PeerIncompatible::NoCipherSuitesInCommon)?;

默认(ignore_client_order: false)按客户端的顺序挑;设成 true 才按服务端的顺序。 3. 组从第一步的列表里取第一个与套件相容的:

        let maybe_skxg = supported_groups
            .iter()
            .find_map(|maybe_skxg| match maybe_skxg {
                Some(skxg) => suite
                    .usable_for_kx_algorithm(skxg.name().key_exchange_algorithm())
                    .then_some(*skxg),
                None => None,
            });

这个列表是按客户端 supported_groups 的顺序排的,ignore_client_order 管不到它。所以组总是按客户端的偏好选,服务端 provider 里 kx_groups 的顺序只决定「支不支持」。

❗IMPORTANT
动脑筋:客户端的组是 [secp256r1, X25519](只为 secp256r1 发了密钥共享),服务端的组是 [X25519, secp256r1],协商出哪个组?会不会有 HelloRetryRequest?

两边都是 ring provider,只改了 CryptoProvider::kx_groups。答案在文末。

一次 handle 发完第一批消息

选完参数,TLS 1.3 交给 tls13::CompleteClientHelloHandling 的 handle_client_hello。它在同一个调用里依次:

  1. 在客户端的 key_share 里找选定组的那一份。没有就发 HelloRetryRequest,返回新的 ExpectClientHello(下一节)。
  2. 如果客户端带了 pre_shared_key,逐个尝试解出会话、核对绑定值(见会话恢复与 0-RTT)。
  3. emit_server_hello:用对方的公钥 start_and_complete 完成密钥交换,发 ServerHello,推进密钥日程,装上握手密钥。
  4. emit_fake_ccs:第一次 ClientHello 就成功时发一个假的 ChangeCipherSpec,给中间设备看(RFC 8446 附录 D.4 的兼容模式);QUIC 不发。
  5. 往一个 HandshakeFlightTls13 里加 EncryptedExtensions、(要客户端证书时)CertificateRequest、Certificate(客户端支持且服务端配了压缩器时发 CompressedCertificate)、CertificateVerify、Finished。恢复会话时跳过证书三件套。
  6. flight.finish 把整批消息放进一条加密记录。
  7. 根据要不要客户端证书、有没有接受 0-RTT,返回 ExpectCertificate、ExpectEarlyData 或 ExpectFinished。

所以完整握手里服务端只经过 ExpectClientHello → ExpectFinished → ExpectTraffic 三个状态;客户端那边要走六个(见客户端 TLS 1.3)。实验里的第一批服务端消息是一次 write_tls 拿到的三条记录:

  flight 2 S->C 664 bytes: Handshake 122 [ServerHello/HRR] | ChangeCipherSpec 1 | ApplicationData 526

526 字节那条记录里装的是哪四条消息、各多长,见一次握手的逐条解密。

send_half_rtt_data(默认 false)打开时,服务端在 Finished 之后马上可以发应用数据,不等客户端的 Finished;要客户端证书时这个选项被忽略,不给未认证的对端发数据。

HelloRetryRequest

ring provider 下,rustls 客户端的 ClientHello 只为一个组发密钥共享(provider 的第一个组,或上次记下的组;aws-lc-rs 默认的后量子组另有规则,见客户端 TLS 1.3)。服务端选中的组不在 key_share 里,就要发 emit_hello_retry_request:

        let req = HelloRetryRequest {
            legacy_version: ProtocolVersion::TLSv1_2,
            session_id,
            cipher_suite: suite.common.suite,
            extensions: HelloRetryRequestExtensions {
                key_share: Some(group),
                supported_versions: Some(ProtocolVersion::TLSv1_3),
                ..Default::default()
            },
        };
        // ...
        transcript.rollup_for_hrr();
        transcript.add_message(&m);
        common.send_msg(m, false);
        common.handshake_kind = Some(HandshakeKind::FullWithHelloRetryRequest);
  • HelloRetryRequest 在线上就是一条 ServerHello,random 是 RFC 规定的固定值;它只告诉客户端「用这个组再来一次」,并且已经定下了套件。
  • rollup_for_hrr 把转录里的第一个 ClientHello 换成它的哈希(RFC 8446 §4.4.1 的 message_hash)。
  • 返回的下一个状态还是 ExpectClientHello,带上 previous_hello。如果第一个 ClientHello 带了 early_data,外面再包一层 ExpectAndSkipRejectedEarlyData,把客户端已经发出、服务端解不开的早期数据按长度丢掉。

第二个 ClientHello 要过几道额外的检查:不能再带 early_data;第一次提供过 PSK 就不能撤回;协商出的套件必须和 HelloRetryRequest 里写的相同(CipherSuiteDifferedOnRetry);这次还没有所选组的密钥共享就是 RefusedToFollowHelloRetryRequest。

📝NOTE
冷知识:第二个 ClientHello 的两道检查是 0.23.45 才加的

9fafe6dd「reject a second ClientHello that drops pre_shared_key」和 185a063b「reject a second ClientHello that changes the cipher suite」都首次发布在 0.23.45。之前服务端在发过 HelloRetryRequest 之后只记一个 done_retry: bool:第二个 ClientHello 撤掉 pre_shared_key、或者客户端改了套件列表让服务端选出另一个套件,都会被接受。两个提交分别加了 offered_psk_before_retry: bool 和 suite_before_retry: Option<CipherSuite> 两个字段,注释引的是 RFC 9846(RFC 8446 的修订版)§4.2.2 和 §4.2.4。随后的 c4e92e8c「Keep data needed for HRR processing together」(同在 0.23.45)把三个字段合成今天的 previous_hello: Option<PreviousClientHello>:Some 本身就表示「发过 HelloRetryRequest」,不会再出现 done_retry 为假、suite_before_retry 却有值这种组合。

实验:客户端 provider 的组改成 [secp256r1, X25519],服务端只有 [X25519]。服务端选 X25519(客户端列表里第二个、服务端唯一支持的),而客户端只发了 secp256r1 的共享:

A default/default: kind Full, group Some(X25519), 4 flights, 1164 bytes
  flight 1 C->S 236 bytes: Handshake 231 [ClientHello]
  flight 2 S->C 664 bytes: Handshake 122 [ServerHello/HRR] | ChangeCipherSpec 1 | ApplicationData 526
  flight 3 C->S 80 bytes: ChangeCipherSpec 1 | ApplicationData 69
  flight 4 S->C 184 bytes: ApplicationData 179
B client[P256,X25519] server[X25519]: kind FullWithHelloRetryRequest, group Some(X25519), 6 flights, 1521 bytes
  flight 1 C->S 267 bytes: Handshake 262 [ClientHello]
  flight 2 S->C 99 bytes: Handshake 88 [ServerHello/HRR] | ChangeCipherSpec 1
  flight 3 C->S 240 bytes: ChangeCipherSpec 1 | Handshake 229 [ClientHello]
  flight 4 S->C 657 bytes: Handshake 122 [ServerHello/HRR] | ApplicationData 525
  flight 5 C->S 74 bytes: ApplicationData 69
  flight 6 S->C 184 bytes: ApplicationData 179

(记录列表是实验程序按 5 字节记录头切出来的;明文握手记录里再按 4 字节消息头列出消息类型,类型 2 既可能是 ServerHello 也可能是 HelloRetryRequest。)

  • 多了两批:88 字节的 HelloRetryRequest 和第二个 ClientHello,等于多一个往返(RTT)。总字节数 1521 对 1164,多 357。
  • 第一个 ClientHello 是 262 字节,比默认的 231 大 31:secp256r1 的公钥 65 字节,X25519 是 32 字节。
  • 假的 ChangeCipherSpec 只发一次:服务端跟在 HelloRetryRequest 后面发,所以 flight 4 里没有;客户端跟在第二个 ClientHello 前面发,所以 flight 5 里没有。
  • 服务端的状态多走了一次 ExpectClientHello,其余不变。

内存里两种握手的耗时(各 300 次,取最小值和中位数;机器负载 5 左右):

time n=300: full min 223 us median 229 us; with HRR min 240 us median 245 us

CPU 上只多 17 µs(多生成的一把 secp256r1 密钥、多处理的两条消息都在里面)。真正的代价是网络上多出的那个往返:内存里没有传输时延,实际连接上它是一个完整的 RTT。

同一个 ClientConfig 第二次连同一个服务器,就不再有 HelloRetryRequest:客户端把服务端选中的组记在会话存储里(kx_hint),下次第一个 ClientHello 直接发这个组的共享,细节见客户端 TLS 1.3。服务端关掉票据(send_tls13_tickets = 0),排除会话恢复的影响:

C2 no tickets, 1st connection: kind FullWithHelloRetryRequest, group Some(X25519), 5 flights, 1338 bytes
C2 no tickets, 2nd connection: kind Full, group Some(X25519), 3 flights, 977 bytes

Acceptor:先读 ClientHello,再给配置

ServerConnection::new 要求一开始就给出 ServerConfig。按 SNI 选证书靠 ResolvesServerCert 就够了;但如果配置本身要变(不同的 ALPN、不同的客户端证书要求、证书要从远处异步取),需要先看见 ClientHello。Acceptor 就是为此准备的:

        pub fn accept(&mut self) -> Result<Option<Accepted>, (Error, AcceptedAlert)> {
            let Some(mut connection) = self.inner.take() else {
                return Err((
                    Error::General("Acceptor polled after completion".into()),
                    AcceptedAlert::empty(),
                ));
            };

            let message = match connection.first_handshake_message() {
                Ok(Some(msg)) => msg,
                Ok(None) => {
                    self.inner = Some(connection);
                    return Ok(None);
                }
                Err(err) => return Err((err, AcceptedAlert::from(connection))),
            };

            let mut cx = Context::from(&mut connection);
            let sig_schemes = match hs::process_client_hello(&message, false, &mut cx) {
                // ...
            };

            Ok(Some(Accepted {
                connection,
                message,
                sig_schemes,
            }))
        }
  • Acceptor 里已经是一个完整的 ConnectionCommon,状态是占位的 Accepting(它的 handle 只返回错误,永远不会被调到)。去帧、拼接跨记录的 ClientHello 都复用普通连接的代码。
  • ClientHello 还不完整时返回 Ok(None),调用方接着 read_tls。
  • 拿到 ClientHello 后只跑 process_client_hello,也就是上面那段与配置无关的检查。
  • 出错时返回 AcceptedAlert,里面是要发给客户端的告警字节,调用方自己写出去。

Accepted::into_connection 拿调用方给的 ServerConfig 新建一个 ExpectClientHello,直接调 with_certified_key 处理那条保存下来的 ClientHello,再把状态换进连接。之后它就是一个普通的 ServerConnection。

实验把 ClientHello 切成两段喂给 Acceptor,按 SNI 选配置,再完成握手;最后喂一段明文 HTTP 请求:

after 100 bytes: accept() -> Ok(false)
after 233 bytes: server_name Some("a.test"), alpn None, 10 cipher suites, 9 signature schemes, named_groups Some([X25519, secp256r1, secp384r1])
handshake via Acceptor: kind Some(Full), sni Some("a.test")
  flight 1 S->C 660 bytes: Handshake 122 [ServerHello/HRR] | ChangeCipherSpec 1 | ApplicationData 522
  flight 2 C->S 80 bytes: ChangeCipherSpec 1 | ApplicationData 69
  flight 3 S->C 184 bytes: ApplicationData 179
plain HTTP: Err(InvalidMessage(InvalidContentType)) "received corrupt message of type InvalidContentType"; alert bytes [21, 3, 3, 0, 2, 2, 50]
  • Ok(false) 就是 Ok(None):233 字节的 ClientHello 只来了 100 字节。
  • 10 个套件是 ring provider 的 3 个 TLS 1.3 套件和 6 个 TLS 1.2 套件,再加 TLS_EMPTY_RENEGOTIATION_INFO_SCSV。
  • 明文 HTTP 请求的第一个字节 G(0x47)不是合法的记录类型。告警字节是一条记录:类型 21(alert)、版本 0x0303、长度 2、级别 2(fatal)、描述 50(decode_error)。
📝NOTE
没有蠢问题

问: 用 Acceptor 给不同的 SNI 发不同的 ServerConfig,这些配置能共用同一个 session_storage 来提高恢复率吗?

答: 能共用,但 rustls 不替你检查后果。ServerConfig 的文档举了例子:配置 A 要求客户端证书、配置 B 不要求,两者共用会话存储时,一个在 B 上没认证过的客户端拿到的会话可以在 A 上恢复,A 会以为它认证过。文档要求共用存储(或 ticketer)的配置在 verifier 和 cert_resolver 上等价,并明确写了「Unlike ClientConfig, rustls does not enforce any policy here」。客户端那边会话值里存着校验器的弱引用,换了校验器就不恢复(见会话恢复与 0-RTT)。

收尾:ExpectFinished

客户端的 Finished 到达,ExpectFinished 用常数时间比较(ConstantTimeEq)核对 verify_data,不对就发 decrypt_error。对了以后:

        let (key_schedule_traffic, resumption) =
            key_schedule_before_finished.into_traffic(self.transcript.current_hash());

        let mut flight = HandshakeFlightTls13::new(&mut self.transcript);
        for _ in 0..self.send_tickets {
            Self::emit_ticket(&mut flight, self.suite, cx, &resumption, &self.config)?;
        }
        flight.finish(cx.common);
  • 读方向这时才换成应用密钥。写方向在发 Finished 时就换了,所以 half-RTT 数据才可能存在。
  • send_tickets 默认是 ServerConfig::send_tls13_tickets 的 2,客户端发了 RFC 9149 的 ticket_request 且服务端设了 max_tls13_tickets 时按客户端的请求取较小值。两张票放进同一批,实验里就是最后那条 179 字节的记录。票据里是什么、存在哪,见会话恢复与 0-RTT。
  • 返回 ExpectTraffic,QUIC 下是 ExpectQuicTraffic,后者拒收一切 TLS 消息,因为 QUIC 连接上的消息走 QUIC 自己的帧(见 TLS 1.2 与其他)。

要客户端证书时中间多 ExpectCertificate(或先经过 ExpectCertificateOrCompressedCertificate)和 ExpectCertificateVerify;客户端证书可选且客户端发了空证书时,ExpectCertificate 直接跳到 ExpectFinished。客户端证书怎么校验见客户端证书与自定义校验。

动脑筋答案

secp256r1,没有 HelloRetryRequest:

D client[P256,X25519] server[X25519,P256]: kind Full, group Some(secp256r1), 4 flights, 1227 bytes
  flight 1 C->S 267 bytes: Handshake 262 [ClientHello]
  flight 2 S->C 697 bytes: Handshake 155 [ServerHello/HRR] | ChangeCipherSpec 1 | ApplicationData 526
  flight 3 C->S 80 bytes: ChangeCipherSpec 1 | ApplicationData 69
  flight 4 S->C 184 bytes: ApplicationData 179

choose_suite_and_kx_group 按客户端 supported_groups 的顺序找第一个服务端支持的组,第一个就是 secp256r1,服务端也支持,于是选它;客户端恰好为它发了共享,不用重试。服务端 provider 把 X25519 排在前面不起作用。ServerHello 是 155 字节而不是 122,多出的 33 字节同样是 secp256r1 公钥比 X25519 长。

小结

  • 服务端在 ExpectClientHello 的一次 handle 里做完所有决定,并把 ServerHello 到 Finished 一起排进队列;完整握手只经过三个状态。
  • 顺序是:与配置无关的检查(process_client_hello)→ 版本 → 证书(ResolvesServerCert)→ 套件和组。证书在套件之前,是 TLS 1.2 套件依赖签名算法的缘故。
  • 套件默认按客户端顺序,ignore_client_order 可改;组总是按客户端顺序,这个开关管不到。
  • 选中的组没有密钥共享就发 HelloRetryRequest:多一个往返,实测多 357 字节、CPU 多 17 µs;客户端记下组之后下次不再触发。
  • Acceptor 把 handle 的两半拆开,先给调用方看 ClientHello,再用调用方选的 ServerConfig 继续。
  • 客户端 Finished 验过之后,服务端换读密钥、默认发 2 张票据,进入 ExpectTraffic。

💬 关联讨论

发起讨论
加载中...