Rust in Everything
基于 rustls 0.23.46

客户端 TLS 1.3

主线位置:握手主线的客户端一侧,从 ClientConnection::new 排好 ClientHello,到收下服务器的 Finished、发出自己的 Finished、进入 ExpectTraffic 收会话票据。每一步推进的密钥在密钥日程,状态机本身怎么运转在状态机,服务器那一侧在服务端 TLS 1.3。

sequenceDiagram
  participant cl as 客户端状态
  participant sv as 服务端
  Note over cl: new 时已排好 ClientHello
  cl->>sv: ClientHello
  Note over cl: ExpectServerHelloOrHelloRetryRequest
  sv->>cl: ServerHello
  Note over cl: 算握手密钥 换解密器
  sv->>cl: EncryptedExtensions
  Note over cl: ExpectEncryptedExtensions
  sv->>cl: Certificate
  Note over cl: ExpectCertificateOrCertReq
  sv->>cl: CertificateVerify
  Note over cl: ExpectCertificateVerify 验链和签名
  sv->>cl: Finished
  Note over cl: ExpectFinished
  cl->>sv: CCS 和 Finished
  Note over cl: ExpectTraffic
  sv->>cl: NewSessionTicket x2

代码在 client/hs.rs(1205 行,ClientHello、ServerHello 和 HelloRetryRequest,TLS 1.2 和 1.3 共用)和 client/tls13.rs(1732 行,ServerHello 之后的 TLS 1.3 状态)。

实验平台:M2 Max、macOS 27、rustc 1.99.0,rustls 0.23.46,ring 和 aws_lc_rs 两个 feature 都开,rustls 的默认 feature(含 prefer-post-quantum、tls12)保持打开;每个配置用 builder_with_provider 显式指定 provider;release 构建,握手全在内存里。服务端证书由 rcgen 现生成(ECDSA P-256,自签),所以服务端第一轮的字节数在不同运行之间差一两个字节(663 或 664)。

ClientHello 在 new 里就排好了

ClientConnection::new 走到 ConnectionCore::for_client:

        let input = ClientHelloInput::new(name, &extra_exts, &mut cx, config)?;
        let state = input.start_handshake(extra_exts, &mut cx)?;
        Ok(Self::new(state, data, common_state))

ClientHelloInput::new 查会话缓存(有没有可恢复的票据)、生成 32 字节随机数和 legacy_session_id,start_handshake 生成密钥共享的私钥,再调 emit_client_hello_for_retry 编码 ClientHello、send_msg 放进待发送队列,返回第一个状态。所以构造完连接对象,wants_write() 已经是 true,第一次 write_tls 就能拿到 ClientHello;密钥生成的开销也算在 new 里。这个函数名里的 for_retry 指它同时负责 HelloRetryRequest 之后的第二个 ClientHello,参数 retryreq 第一次是 None。

逐个扩展

实验程序 hsa-lab/src/bin/clienthello.rs 构造客户端(SNI localhost、空根证书库、不加 ALPN),把第一次 write_tls 写出的字节按 RFC 8446 的格式逐字段拆开。ring provider:

== ring (alpn=false): 236 bytes on the wire
record: type 22 version 0301 len 231
handshake: type 1 len 227
legacy_session_id: 32 bytes
cipher_suites: 10 entries [TLS13_AES_256_GCM_SHA384, TLS13_AES_128_GCM_SHA256, TLS13_CHACHA20_POLY1305_SHA256, TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256]
extensions: 134 bytes
     35 session_ticket               0B
     45 psk_key_exchange_modes       2B
      0 server_name                 14B
     11 ec_point_formats             2B
     23 extended_master_secret       0B
     43 supported_versions           5B ["0304", "0303"]
     10 supported_groups             8B ["X25519", "secp256r1", "secp384r1"]
     51 key_share                   38B X25519:32B
     13 signature_algorithms        20B 9 schemes ["ECDSA_NISTP384_SHA384", "ECDSA_NISTP256_SHA256", "ED25519", "RSA_PSS_SHA512", "RSA_PSS_SHA384", "RSA_PSS_SHA256", "RSA_PKCS1_SHA512", "RSA_PKCS1_SHA384", "RSA_PKCS1_SHA256"]
      5 status_request               5B

先看扩展之外的字段:

  • 记录版本 0301,握手里的 legacy_version 是 0303。emit_client_hello_for_retry 末尾特意为第一个 ClientHello 选 TLSv1_0,注释引了 RFC 8446 §5.1「MAY also be 0x0301 for compatibility purposes」;HelloRetryRequest 之后的第二个 ClientHello 用 0303。
  • legacy_session_id 是 32 个随机字节。TLS 1.3 不用它,但 RFC 8446 附录 D.4 的「中间设备兼容模式」要求非空;ClientHelloInput::new 只在 QUIC 或不支持 TLS 1.3 时把它置空。
  • 10 个套件:provider 的 9 个,加上末尾的 TLS_EMPTY_RENEGOTIATION_INFO_SCSV(0x00ff),只在提议 TLS 1.2 时加。rustls 用它代替 renegotiation_info 扩展,表示「我支持安全重协商」,实际上什么重协商都不做。

扩展逐个对应到 emit_client_hello_for_retry 里的代码:

扩展内容来源
server_namelocalhostconfig.enable_sni 且 ServerName 是 DNS 名;用 IP 连时不发
supported_versions1.3、1.2with_safe_default_protocol_versions;tls12 feature 关掉或配置只要 1.3 时只有 0304
supported_groupsprovider 的 kx_groups,按顺序过滤掉不适用于所提议版本的组
key_share一个或两个,见下节initial_key_share
signature_algorithms校验器的 supported_verify_schemes()来自 WebPkiSupportedAlgorithms,见服务器证书
psk_key_exchange_modes只有 psk_dhe_ke注释:纯 PSK 模式「don't have forward secrecy」,所以不提议
status_requestOCSP每次都发,CertificateStatusRequest::build_ocsp()
extended_master_secret空每次都发,只对 TLS 1.2 有意义
ec_point_formats只有 uncompressedprovider 里有 ECDHE 组时发,也是 TLS 1.2 的遗留
session_ticket空(请求票据)TLS 1.2 的票据,见 prepare_resumption:支持 TLS 1.2 且 tls12_resumption 是默认的 SessionIdOrTickets 时发

alpn、pre_shared_key、early_data、compress_certificate、certificate_authorities、cookie、ECH 都是按条件出现的:配置了 alpn_protocols 才有 ALPN(加 h2、http/1.1 两个协议后 ClientHello 是 254 字节,多 18 字节),有可恢复的 TLS 1.3 票据才有 PSK(见会话恢复与 0-RTT),开了 brotli / zlib feature 才提议证书压缩,HelloRetryRequest 带了 cookie 才回 cookie。

同一个程序第二次构造客户端,扩展的顺序变了,内容和总长度不变:

== ring (alpn=false): 236 bytes on the wire
...
     10 supported_groups             8B ["X25519", "secp256r1", "secp384r1"]
     11 ec_point_formats             2B
      5 status_request               5B
     51 key_share                   38B X25519:32B
     35 session_ticket               0B
     45 psk_key_exchange_modes       2B
     23 extended_master_secret       0B
     13 signature_algorithms        20B 9 schemes [...]
     43 supported_versions           5B ["0304", "0303"]
      0 server_name                 14B

order_insensitive_extensions_in_random_order 用每个连接一个的 16 位随机种子(ClientHelloDetails 里的 extension_order_seed)给扩展排序,只有 ECH 和 pre_shared_key 固定排在最后(RFC 要求 PSK 必须是最后一个扩展)。种子存在连接里,HelloRetryRequest 之后的第二个 ClientHello 用同一个顺序。

📝NOTE
冷知识:扩展顺序是 0.23 开始随机的

0.22 及以前,ClientHello 的扩展按代码里 push 的先后排列,每个 rustls 客户端发出的顺序都一样,成了识别「这是 rustls」的指纹之一。提交 c296594d(「Randomize ClientHello extensions」,首次发布在 0.23.0)引用了 Chrome 随机化 ClientHello 扩展的提案,提交说明写的是:「This commit sorts all the extensions that have been sent in the same order as before by using a seed that is saved at the start of the connection. And keeps the PSK extension in the end.」也就是上面的 order_seed:每个连接随机一次,同一个连接内保持不变。

发几个密钥共享

initial_key_share 只为一个组生成私钥:会话缓存里记着这个服务器上次用的组(kx_hint)就用它,否则取 provider 的 kx_groups 的第一个。ring 的默认组是 X25519、P-256、P-384,所以只发一个 32 字节的 X25519 公钥。

aws-lc-rs 的情况不同。rustls 的默认 feature 里有 prefer-post-quantum,它让 aws-lc-rs 的 DEFAULT_KX_GROUPS 把混合组 X25519MLKEM768 排到第一个。同一个程序换成 aws-lc-rs:

== aws-lc-rs (alpn=false): 1466 bytes on the wire
record: type 22 version 0301 len 1461
handshake: type 1 len 1457
legacy_session_id: 32 bytes
cipher_suites: 10 entries [...]
extensions: 1364 bytes
     43 supported_versions           5B ["0304", "0303"]
     35 session_ticket               0B
     10 supported_groups            10B ["X25519MLKEM768", "X25519", "secp256r1", "secp384r1"]
     51 key_share                 1258B X25519MLKEM768:1216B X25519:32B
      5 status_request               5B
      0 server_name                 14B
     13 signature_algorithms        28B 13 schemes ["ECDSA_NISTP384_SHA384", "ECDSA_NISTP256_SHA256", "ECDSA_NISTP521_SHA512", "ED25519", "RSA_PSS_SHA512", "RSA_PSS_SHA384", "RSA_PSS_SHA256", "RSA_PKCS1_SHA512", "RSA_PKCS1_SHA384", "RSA_PKCS1_SHA256", "ML_DSA_44", "ML_DSA_65", "ML_DSA_87"]
     23 extended_master_secret       0B
     11 ec_point_formats             2B
     45 psk_key_exchange_modes       2B

key_share 里有两项:1216 字节的 X25519MLKEM768(ML-KEM-768 的 1184 字节封装公钥加 X25519 的 32 字节)和一个 32 字节的 X25519。第二项是「顺带」的,代码在 emit_client_hello_for_retry 里:

            if let Some((component_group, component_share)) =
                key_share
                    .hybrid_component()
                    .filter(|(group, _)| {
                        config
                            .find_kx_group(*group, ProtocolVersion::TLSv1_3)
                            .is_some()
                    })
            {
                shares.push(KeyShareEntry::new(component_group, component_share));
            }

混合组的私钥里本来就有一个 X25519 私钥,hybrid_component() 把它的公钥单独拿出来,只要 provider 也单独支持 X25519,就再放一项。服务器如果不支持后量子组、选了 X25519,客户端用同一个私钥完成(KeyExchangeChoice::Component),不必多一轮 HelloRetryRequest。两项加起来,ClientHello 从 236 字节涨到 1466 字节,多出来的 1230 字节里 1184 字节是 ML-KEM 公钥,剩下 46 字节是多一项密钥共享的 32 字节公钥和 4 字节项头、多出的四个签名算法(8 字节)和一个组名(2 字节)。签名算法多了 ECDSA_NISTP521_SHA512 和三个 ML-DSA,是 aws-lc-rs 的 WebPkiSupportedAlgorithms 比 ring 多支持的。两个 provider 的组和套件对比、prefer-post-quantum 关掉后的情况,见套件与密钥交换。

1466 字节比以太网上常见的 TCP 最大段长 1460 字节还多 6 字节,这样的 ClientHello 在 TCP 上通常要分两个段发。

ServerHello:选版本、套件,换上握手密钥

客户端的第一个状态是 ExpectServerHelloOrHelloRetryRequest,它只是分派器:ServerHello 交给内部的 ExpectServerHello,HelloRetryRequest 交给 handle_hello_retry_request。

ExpectServerHello::handle 做版本和套件的协商检查,每一项不过都有对应的告警:

  1. 版本:legacy_version 是 1.2 时看 supported_versions 扩展,得到 1.3 还是 1.2;配置不支持就发 protocol_version。
  2. 压缩方法必须是 null,服务器回了没请求过的扩展发 unsupported_extension。
  3. 套件必须在自己提议的列表里(offered_cipher_suites),且和版本匹配;HelloRetryRequest 之后还必须和 HelloRetryRequest 里的套件相同。
  4. 套件定下来,哈希算法也就定了:transcript_buffer.start_hash(suite.hash_provider()) 把之前缓存的 ClientHello 字节灌进哈希,转为增量哈希,再加入 ServerHello。

然后转到 tls13::handle_server_hello:

  • validate_server_hello 检查 ServerHello 只带 key_share、pre_shared_key、supported_versions 三种明文扩展,其他扩展(如 ALPN)在 TLS 1.3 里必须放进加密的 EncryptedExtensions。
  • 服务器的密钥共享组必须是我们发过的那个(或混合组的分量),否则 illegal_parameter。
  • 没有选 PSK 时,丢掉早期数据的密钥日程,KeySchedulePreHandshake::new(suite);用 ECDHE 共享秘密 into_handshake,再用 Hash(ClientHello..ServerHello) 派生出两个握手流量密钥,装上解密器(服务器的握手密钥);没有早期数据时也装上加密器。
  • check_aligned_handshake():换密钥时,不允许缓冲区里还有半条握手消息,否则这条消息的前后两半会落在不同的保护下。
  • emit_fake_ccs:发出那条兼容中间设备的 CCS,只在这个连接上发一次。实测它和客户端 Finished 一起出现在第二轮的 80 字节里(6 + 74)。

返回 ExpectEncryptedExtensions,它手里的密钥日程是 KeyScheduleHandshake。

EncryptedExtensions:分岔点

ExpectEncryptedExtensions::handle 处理第一条加密消息:

  • validate_encrypted_extensions:不能出现没请求过的扩展,也不能出现只该在 ServerHello 里的扩展或 TLS 1.3 禁用的扩展(ec_point_formats、session_ticket、renegotiation_info、extended_master_secret),违反就 unsupported_extension。
  • ALPN 在这里定:process_alpn_protocol,check_selected_alpn 打开(默认)时服务器选了没提议的协议就 illegal_parameter。
  • QUIC 连接必须有传输参数扩展。
  • ECH:服务器拒绝时把它给的 retry_configs 记下来,到握手末尾才报错(见 TLS 1.2 与其他)。

然后按「是不是会话恢复」分岔。恢复时服务器不发证书,状态直接跳到 ExpectFinished,对端证书用缓存里的那条链,两个校验凭据用 assertion() 直接造出来,代码注释写明:「We don't reverify the certificate chain here: resumption is a continuation of the previous session in terms of security policy.」完整握手则进入 ExpectCertificateOrCertReq(或提议了证书压缩时的 ExpectCertificateOrCompressedCertificateOrCertReq),等 Certificate,或者先来一个要求客户端证书的 CertificateRequest。

同一程序连续做两次握手、共用一个 ClientConfig,第二次就是恢复,客户端只经过三个状态:

== second handshake, same configs (resumption)
[hsa] Client: rustls::client::hs::ExpectServerHelloOrHelloRetryRequest + ServerHello -> rustls::client::tls13::ExpectEncryptedExtensions
[hsa] Client: rustls::client::tls13::ExpectEncryptedExtensions + EncryptedExtensions -> rustls::client::tls13::ExpectFinished
[hsa] Client: rustls::client::tls13::ExpectFinished + Finished -> rustls::client::tls13::ExpectTraffic
-- round 1: c->s 327 bytes, s->c 219 bytes

Certificate 和 CertificateVerify

服务器的证书链和对握手的签名是两条消息,客户端也就是两个状态,但真正的校验全在第二个里。

ExpectCertificate::handle 只做三件事:把消息加入转录哈希;检查 certificate_request_context 为空(只有客户端证书才用这个字段,非空就是 decode_error);把证书链和叶子证书的 OCSP 响应 into_owned() 收起来,交给 ExpectCertificateVerify。

ExpectCertificateVerify::handle 按顺序:

        // 1. Verify the certificate chain.
        let (end_entity, intermediates) = self
            .server_cert
            .cert_chain
            .split_first()
            .ok_or(Error::NoCertificatesPresented)?;

        let now = self.config.current_time()?;

        let cert_verified = self
            .config
            .verifier
            .verify_server_cert(
                end_entity,
                intermediates,
                &self.server_name,
                &self.server_cert.ocsp_response,
                now,
            )
            .map_err(|err| {
                cx.common
                    .send_cert_verify_error_alert(err)
            })?;

        // 2. Verify their signature on the handshake.
        let handshake_hash = self.transcript.current_hash();
        let sig_verified = self
            .config
            .verifier
            .verify_tls13_signature(
                construct_server_verify_message(&handshake_hash).as_ref(),
                end_entity,
                cert_verify,
            )
            .map_err(|err| {
                cx.common
                    .send_cert_verify_error_alert(err)
            })?;

签名的输入是「64 个空格 + TLS 1.3, server CertificateVerify + 0 + 到 Certificate 为止的转录哈希」(construct_server_verify_message),所以算哈希必须在把 CertificateVerify 自己加进转录之前,代码里 add_message 确实在最后。链的校验推迟到这里,和签名校验连着做:只有链通过了,才用叶子证书的公钥去验签名,两个结果 ServerCertVerified、HandshakeSignatureValid 一起搬进 ExpectFinished。如果中间夹着 CertificateRequest,它在 Certificate 之前到达,由 ExpectCertificateRequest 处理后再进 ExpectCertificate。verify_server_cert 里面做了什么,是校验主线的内容,见服务器证书。

Finished:先验对方,再发自己的

ExpectFinished::handle 是客户端最长的一步:

  1. 用 key_schedule.sign_server_finish(&handshake_hash) 算出期望的 verify_data,和服务器发来的做常量时间比较(subtle::ConstantTimeEq),不等就 decrypt_error。
  2. 把服务器 Finished 加入转录,得到 hash_after_handshake。有早期数据时这里补发 EndOfEarlyData,并把加密器从早期数据密钥换成握手密钥。
  3. 服务器要了客户端证书的话,发 Certificate(和 CertificateVerify),仍用握手密钥加密。
  4. into_pre_finished_client_traffic:用 hash_after_handshake 派生两个应用流量密钥和导出密钥,同时用客户端握手密钥算出自己的 Finished;发出 Finished。
  5. set_kx_hint:把这次服务器选的组记进会话缓存;remove_tls12_session:确认对方支持 1.3 之后,删掉这个服务器的 TLS 1.2 会话,避免以后降级恢复。
  6. into_traffic:现在才把应用流量密钥装进记录层,同时派生恢复主密钥(KeyScheduleResumption)。
  7. ECH 被拒绝时在这里报错,然后 start_traffic 把握手期间缓冲的明文发出去,返回 ExpectTraffic。

第 4 步和第 6 步分开,是因为客户端的 Finished 必须用握手密钥加密,而应用密钥的推导又只依赖到服务器 Finished 为止的转录,所以中间有一个「算好了但还没装上」的 KeyScheduleClientBeforeFinished,见密钥日程。

ExpectTraffic 接受三种消息:应用数据、NewSessionTicket、KeyUpdate。实测服务器在第二轮发了两张票(184 字节),客户端每收一张就用 KeyScheduleResumption::derive_ticket_psk 算出 PSK,存进会话缓存,状态不变。默认发两张票是服务端的配置(send_tls13_tickets),见会话恢复与 0-RTT。

HelloRetryRequest

服务器不支持客户端猜的组时,回一个 HelloRetryRequest 指定组。实验让服务端 provider 只保留 secp256r1,客户端用默认的 ring provider(先猜 X25519):

== HelloRetryRequest (server only secp256r1), ring
[hsa] Client: rustls::client::hs::ExpectServerHelloOrHelloRetryRequest + HelloRetryRequest -> rustls::client::hs::ExpectServerHello
-- round 1: c->s 236 bytes, s->c 99 bytes
[hsa] Client: rustls::client::hs::ExpectServerHello + ServerHello -> rustls::client::tls13::ExpectEncryptedExtensions
...
-- round 2: c->s 275 bytes, s->c 689 bytes
-- round 3: c->s 74 bytes, s->c 184 bytes
-- done: Some(TLSv1_3) Some(TLS13_AES_256_GCM_SHA384) kx Some(secp256r1) kind Some(FullWithHelloRetryRequest)

多了一整轮。第二轮客户端的 275 字节是 6 字节的 CCS(HelloRetryRequest 之后先发假 CCS)加 269 字节的新 ClientHello:密钥共享从 32 字节的 X25519 换成 65 字节的未压缩 P-256 点,正好多 33 字节。第三轮只剩 74 字节的 Finished,CCS 已经发过。

handle_hello_retry_request 拒绝几类不合理的重试,全部是 illegal_parameter:没有 cookie 却要求一个已经发过的组、cookie 为空、什么都不要求改、legacy_session_id 没有原样回显、版本不是 1.3、套件没提议过、要求的组 provider 不支持。通过后,HelloRetryRequest 选定的套件就是最终套件(ExpectServerHello 会检查 ServerHello 和它一致),转录哈希变成「ClientHello1 的哈希 + HelloRetryRequest」(into_hrr_buffer,见密钥日程),早期数据作废,再调一次 emit_client_hello_for_retry,这次 retryreq 是 Some,返回的状态是只认 ServerHello 的 ExpectServerHello。

❗IMPORTANT
动脑筋:同一个 ClientConfig 再连一次这个只要 secp256r1 的服务器,还会收到 HelloRetryRequest 吗?换一个新的会话缓存呢?

提示:ExpectFinished 第 5 步。答案在文末。

动脑筋答案

同一个配置再连不会;换新的会话缓存又会。实验程序接着上面的握手跑了两次:

== again, same configs after HRR
[hsa] Client: rustls::client::hs::ExpectServerHelloOrHelloRetryRequest + ServerHello -> rustls::client::tls13::ExpectEncryptedExtensions
[hsa] Client: rustls::client::tls13::ExpectEncryptedExtensions + EncryptedExtensions -> rustls::client::tls13::ExpectFinished
[hsa] Client: rustls::client::tls13::ExpectFinished + Finished -> rustls::client::tls13::ExpectTraffic
-- round 1: c->s 360 bytes, s->c 252 bytes
-- round 2: c->s 80 bytes, s->c 184 bytes
-- done: Some(TLSv1_3) Some(TLS13_AES_256_GCM_SHA384) kx Some(secp256r1) kind Some(Resumed)
== fresh session store, same server
[hsa] Client: rustls::client::hs::ExpectServerHelloOrHelloRetryRequest + HelloRetryRequest -> rustls::client::hs::ExpectServerHello
-- round 1: c->s 236 bytes, s->c 99 bytes
...
-- done: Some(TLSv1_3) Some(TLS13_AES_256_GCM_SHA384) kx Some(secp256r1) kind Some(FullWithHelloRetryRequest)
  • 第一次握手结束时,ExpectFinished 调了 set_kx_hint(server_name, secp256r1)。第二次 initial_key_share 先查 kx_hint,直接生成 P-256 的密钥共享,服务器接受,ServerHello 一次到位。这次同时也是会话恢复(缓存里有上次的票),所以 ClientHello 是 360 字节,比 HelloRetryRequest 之后的 269 字节多出了 PSK 扩展。
  • 提示存在 ClientSessionStore 里(set_kx_hint 是这个 trait 的方法),和票据放在一起。新配置换成 Resumption::in_memory_sessions(256),提示没了,又从 provider 的第一个组 X25519 猜起,HelloRetryRequest 重现。进程重启后内存缓存清空,效果相同。

小结

  • ClientHello 在 ClientConnection::new 里就生成并排队;第一个 ClientHello 的记录版本是 0301,legacy_session_id 是 32 个随机字节,套件列表末尾有 TLS_EMPTY_RENEGOTIATION_INFO_SCSV。
  • 默认配置下 ring 的 ClientHello 236 字节、一个 X25519 共享;aws-lc-rs(默认开 prefer-post-quantum)1466 字节,X25519MLKEM768 1216 字节加一个顺带的 X25519 32 字节。
  • 扩展顺序每个连接随机一次(0.23.0 起),只有 ECH 和 PSK 固定在最后。
  • 完整握手客户端停留六个状态:ExpectServerHelloOrHelloRetryRequest → ExpectEncryptedExtensions → ExpectCertificateOrCertReq → ExpectCertificateVerify → ExpectFinished → ExpectTraffic;会话恢复从 EncryptedExtensions 直接跳到 Finished。
  • 证书链和签名都在 ExpectCertificateVerify 里校验,签名覆盖的转录到 Certificate 为止;服务器 Finished 用常量时间比较。
  • HelloRetryRequest 多一整轮;ExpectFinished 把服务器选的组记进会话缓存,同一缓存下次直接猜中。

💬 关联讨论

发起讨论
加载中...