Rust in Everything
基于 rustls 0.23.46

密钥日程

主线位置:握手主线里和状态并排走的另一条链。客户端收到 ServerHello 时,密钥日程从「早期秘密」推进到「握手秘密」,装上握手流量密钥;收到服务器 Finished 时推进到「主秘密」,算出应用流量密钥和恢复主密钥。每一步的输入都有一个转录哈希。本页讲这条链在代码里的类型、它怎么防止用错顺序、转录哈希怎么维护,并用外部测试向量核对一步推导。
flowchart TB
  early["KeyScheduleEarly 有 PSK"] -->|From| pre["KeySchedulePreHandshake"]
  pre0["new 无 PSK"] --> pre
  pre -->|"into_handshake ECDHE"| hstart["KeyScheduleHandshakeStart"]
  hstart -->|derive_client_handshake_secrets| hs["KeyScheduleHandshake"]
  hstart -->|derive_server_handshake_secrets| hs
  hs -->|客户端| cbf["KeyScheduleClientBeforeFinished"]
  hs -->|服务端| pend["TrafficWithClientFinishedPending"]
  pend -->|sign_client_finish| bf["KeyScheduleBeforeFinished"]
  cbf -->|into_traffic| traffic["KeyScheduleTraffic"]
  bf -->|into_traffic| traffic
  cbf --> res["KeyScheduleResumption"]
  bf --> res

全部在 tls13/key_schedule.rs,1311 行,其中 1041 行起是测试;转录哈希在 hash_hs.rs,347 行。HKDF 本身由 provider 提供(Tls13CipherSuite::hkdf_provider),这一层只管「用什么标签、什么上下文、按什么顺序」。

实验平台:M2 Max、macOS 27、rustc 1.99.0,rustls 0.23.46(ring 和 aws-lc-rs 两个 provider),release 构建。

RFC 的阶梯和代码里的类型

RFC 8446 §7.1 的密钥日程是三级 HKDF-Extract,每级之间用 Derive-Secret(., "derived", "") 做盐:

             0
             |
   PSK ->  HKDF-Extract = Early Secret      -> binder_key, client_early_traffic_secret, ...
             |
       Derive-Secret(., "derived", "")
             |
 (EC)DHE -> HKDF-Extract = Handshake Secret -> c hs traffic, s hs traffic
             |
       Derive-Secret(., "derived", "")
             |
     0 ->  HKDF-Extract = Master Secret     -> c ap traffic, s ap traffic, exp master, res master

rustls 把每一级、甚至每一级里的半步,做成一个类型:

阶段类型进入方式能做的事
Early Secret(有 PSK)KeyScheduleEarlynew(suite, psk)算 PSK 绑定值、client_early_traffic_secret
Early Secret(待输入 ECDHE)KeySchedulePreHandshakenew(suite)(无 PSK)或 From<KeyScheduleEarly>只能 into_handshake
Handshake Secret(刚算出)KeyScheduleHandshakeStartinto_handshake(shared_secret)派生两个握手流量密钥(分客户端、服务端两个方法),ECH 确认
Handshake Secret(密钥已派生)KeyScheduleHandshakederive_*_handshake_secrets算服务器 Finished,换握手加密 / 解密器
Master Secret,客户端 Finished 未发KeyScheduleClientBeforeFinishedinto_pre_finished_client_traffic应用密钥已派生,但还没装进记录层
Master Secret,等客户端 FinishedKeyScheduleTrafficWithClientFinishedPendinginto_traffic_with_client_finished_pending(服务端)已用应用密钥加密,验客户端 Finished
应用阶段KeyScheduleTrafficinto_trafficKeyUpdate、导出密钥材料、取出流量密钥
恢复KeyScheduleResumption和 KeyScheduleTraffic 一起产生由票据 nonce 算 PSK

文件开头的注释说明了用意:

// We express the state of a contained KeySchedule using these
// typestates.  This means we can write code that cannot accidentally
// (e.g.) encrypt application data using a KeySchedule solely constructed
// with an empty or trivial secret, or extract the wrong kind of secrets
// at a given point.

签名就是顺序

每一步都按值拿走 self,返回下一个类型:

impl KeySchedulePreHandshake {
    pub(crate) fn new(suite: &'static Tls13CipherSuite) -> Self {
    // ...
    pub(crate) fn into_handshake(
        mut self,
        shared_secret: SharedSecret,
    ) -> KeyScheduleHandshakeStart {
impl KeyScheduleHandshakeStart {
    pub(crate) fn derive_client_handshake_secrets(
        mut self,
        early_data_enabled: bool,
        hs_hash: hash::Output,
        suite: &'static Tls13CipherSuite,
        key_log: &dyn KeyLog,
        client_random: &[u8; 32],
        common: &mut CommonState,
    ) -> KeyScheduleHandshake {
impl KeyScheduleHandshake {
    pub(crate) fn sign_server_finish(&self, hs_hash: &hash::Output) -> hmac::Tag {
    // ...
    pub(crate) fn into_pre_finished_client_traffic(
        self,
        pre_finished_hash: hash::Output,
        handshake_hash: hash::Output,
        key_log: &dyn KeyLog,
        client_random: &[u8; 32],
    ) -> (KeyScheduleClientBeforeFinished, hmac::Tag) {

读签名就能看出约束:

  • 跳步不可能:KeySchedulePreHandshake 上只有 into_handshake 一个方法,不给 ECDHE 共享秘密就拿不到任何流量密钥。
  • 重复不可能:into_handshake、derive_client_handshake_secrets、into_pre_finished_client_traffic 都消耗 self,同一个握手秘密不能派生两次。
  • 在错的阶段取错的密钥不可能:sign_server_finish 只在 KeyScheduleHandshake 上有,因为它用的是服务端握手流量密钥;到了 KeyScheduleTraffic,握手流量密钥已经被丢掉了,字段里只剩三个当前的应用阶段秘密和 KeyScheduleSuite(只有一个套件指针),连主秘密本身都不在了,再想推导什么都推不出来。

这些类型都是 pub(crate),tls13 模块也是私有的,从外面写代码连名字都引用不到。要看编译器怎么拦,只能在 crate 里面试。实验把 cargo 缓存里的 rustls-0.23.46 拷到 hsa-patched/rustls,在 key_schedule.rs 末尾加三个故意写错的函数:

fn hsa_skip_a_step(suite: &'static Tls13CipherSuite, hs_hash: hash::Output, common: &mut CommonState) {
  let pre = KeySchedulePreHandshake::new(suite);
  // forgot into_handshake(shared_secret)
  let _hs = pre.derive_client_handshake_secrets(false, hs_hash, suite, &crate::NoKeyLog, &[0u8; 32], common);
}

fn hsa_reuse(suite: &'static Tls13CipherSuite, h1: hash::Output, h2: hash::Output, common: &mut CommonState) {
  let start = KeySchedulePreHandshake::new(suite).into_handshake(SharedSecret::from(&[0u8; 32][..]));
  let _a = start.derive_client_handshake_secrets(false, h1, suite, &crate::NoKeyLog, &[0u8; 32], common);
  let _b = start.derive_client_handshake_secrets(false, h2, suite, &crate::NoKeyLog, &[0u8; 32], common);
}

fn hsa_wrong_stage(ks: &KeyScheduleTraffic, hs_hash: &hash::Output) {
  // the server Finished key lives only in KeyScheduleHandshake
  let _tag = ks.sign_server_finish(hs_hash);
}

cargo check 的输出(原样,只删了 Checking 行):

error[E0599]: no method named `derive_client_handshake_secrets` found for struct `KeySchedulePreHandshake` in the current scope
    --> ~/exp-rustls/hsa-patched/rustls/src/tls13/key_schedule.rs:1318:19
     |
 114 | pub(crate) struct KeySchedulePreHandshake {
     | ----------------------------------------- method `derive_client_handshake_secrets` not found for this struct
...
1318 |     let _hs = pre.derive_client_handshake_secrets(false, hs_hash, suite, &crate::NoKeyLog, &[0u8; 32], common);
     |                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ method not found in `KeySchedulePreHandshake`

error[E0599]: no method named `sign_server_finish` found for reference `&KeyScheduleTraffic` in the current scope
    --> ~/exp-rustls/hsa-patched/rustls/src/tls13/key_schedule.rs:1331:19
     |
1331 |     let _tag = ks.sign_server_finish(hs_hash);
     |                   ^^^^^^^^^^^^^^^^^^ method not found in `&KeyScheduleTraffic`

error[E0382]: use of moved value: `start`
    --> ~/exp-rustls/hsa-patched/rustls/src/tls13/key_schedule.rs:1325:14
     |
1323 |     let start = KeySchedulePreHandshake::new(suite).into_handshake(SharedSecret::from(&[0u8; 32][..]));
     |         ----- move occurs because `start` has type `KeyScheduleHandshakeStart`, which does not implement the `Copy` trait
1324 |     let _a = start.derive_client_handshake_secrets(false, h1, suite, &crate::NoKeyLog, &[0u8; 32], common);
     |                    --------------------------------------------------------------------------------------- `start` moved due to this method call
1325 |     let _b = start.derive_client_handshake_secrets(false, h2, suite, &crate::NoKeyLog, &[0u8; 32], common);
     |              ^^^^^ value used here after move
     |
note: `tls13::key_schedule::KeyScheduleHandshakeStart::derive_client_handshake_secrets` takes ownership of the receiver `self`, which moves `start`
    --> ~/exp-rustls/hsa-patched/rustls/src/tls13/key_schedule.rs:158:13
     |
 158 |     mut self,
     |             ^^^^

Some errors have detailed explanations: E0382, E0599.
For more information about an error, try `rustc --explain E0382`.
error: could not compile `rustls` (lib) due to 3 previous errors

类型链管不到的是「传进来的哈希对不对」:hs_hash: hash::Output 只是一段字节,传成别的时刻的转录哈希,编译器不会发现,握手会在对端验 Finished 时失败。这部分靠下面的转录哈希和测试。

derive_client_handshake_secrets 末尾有一处分支:

        // Decrypt with the peer's key, encrypt with our own key
        new.ks
            .set_decrypter(&new.server_handshake_traffic_secret, common);

        if !early_data_enabled {
            // Set the client encryption key for handshakes if early data is not used
            new.ks
                .set_encrypter(&new.client_handshake_traffic_secret, common);
        }

发着早期数据的客户端,加密器还要留在早期流量密钥上,直到发出 EndOfEarlyData 才由 set_handshake_encrypter 换掉(客户端 TLS 1.3 的 ExpectFinished 第 2 步)。

客户端和服务端在 Master Secret 这一级走的是两条不同的路。服务端发完自己的 Finished 就可以用应用密钥发数据(0.5-RTT),所以 into_traffic_with_client_finished_pending 立即装上服务端的应用加密器,同时还留着客户端握手流量密钥,用来验客户端的 Finished;验过之后(sign_client_finish)才装上客户端应用密钥做解密器。客户端则是反过来:应用密钥先派生出来,但自己的 Finished 必须用握手密钥加密,所以 KeyScheduleClientBeforeFinished 把它们攥在手里,等 Finished 发出后的 into_traffic 才一次装上两个方向。

📝NOTE
冷知识:类型链是 2019 年从一个带 Option 的结构里拆出来的

0.16 及以前,KeySchedule 是一个结构,把当前秘密、哈希算法和 current_client_traffic_secret、current_server_traffic_secret、current_exporter_secret 三个 Option 字段放在一起,调用方按约定的顺序调方法。提交 d2c1e6c4(「Typestate-ify TLS1.3 key schedule」,首次发布在 0.17.0)把它拆成 KeyScheduleEarly、KeyScheduleNonSecret、KeyScheduleHandshake 等类型,提交说明列出要防的三类错误:「extracting the wrong key at a given point, failing to input secret data, encrypting data with fixed keys」。第一版的 KeyScheduleHandshake 里流量秘密仍是 Option<hkdf::Prk>,sign_server_finish 里写着 self.current_server_traffic_secret.as_ref().unwrap()。提交 c7dbe06f(「Use type state to avoid unwrapping in handshake KeySchedule」,首次发布在 0.20.0)多拆出 KeyScheduleHandshakeStart,派生完才构造 KeyScheduleHandshake,字段直接是秘密本身,不再有 Option;提交 aa901bd1(0.20.3)把 KeyScheduleNonSecret 改名为今天的 KeySchedulePreHandshake。

底下只有一个 KeySchedule

所有类型里都包着同一个私有结构:

struct KeySchedule {
    current: Box<dyn HkdfExpander>,
    inner: KeyScheduleSuite,
}

current 是当前这一级 HKDF-Extract 的结果(PRK),以「扩展器」的形式存着,取不出原始字节。进下一级只有两个入口:

    fn input_empty(&mut self) {
        let salt = self.derive_for_empty_hash(SecretKind::DerivedSecret);
        self.current = self
            .suite
            .hkdf_provider
            .extract_from_zero_ikm(Some(salt.as_ref()));
    }

    /// Input the given secret.
    fn input_secret(&mut self, secret: &[u8]) {
        let salt = self.derive_for_empty_hash(SecretKind::DerivedSecret);
        self.current = self
            .suite
            .hkdf_provider
            .extract_from_secret(Some(salt.as_ref()), secret);
    }

就是阶梯里「Derive-Secret(., "derived", "") 做盐,再 Extract」那一格:input_secret 输入 ECDHE 共享秘密进入 Handshake Secret,input_empty 输入全零进入 Master Secret。derive_for_empty_hash 里的空串哈希,SHA-256 和 SHA-384 用的是预先算好的常量(hash_for_empty_input),不现场算。

派生某个秘密是 derive → hkdf_expand_label_block,最终落在 hkdf_expand_label_inner,它按 RFC 8446 的 HkdfLabel 结构拼出 info,不拼接成一个缓冲区,而是以切片数组交给 provider:

    const LABEL_PREFIX: &[u8] = b"tls13 ";

    let output_len = u16::to_be_bytes(n as u16);
    let label_len = u8::to_be_bytes((LABEL_PREFIX.len() + label.len()) as u8);
    let context_len = u8::to_be_bytes(context.len() as u8);

    let info = &[
        &output_len[..],
        &label_len[..],
        LABEL_PREFIX,
        label,
        &context_len[..],
        context,
    ];

标签由 SecretKind 给出,其中六种还有写进 SSLKEYLOGFILE 的名字:

SecretKind标签KeyLog 名字
ResumptionPskBinderKeyres binder不记
ClientEarlyTrafficSecretc e trafficCLIENT_EARLY_TRAFFIC_SECRET
ClientHandshakeTrafficSecretc hs trafficCLIENT_HANDSHAKE_TRAFFIC_SECRET
ServerHandshakeTrafficSecrets hs trafficSERVER_HANDSHAKE_TRAFFIC_SECRET
ClientApplicationTrafficSecretc ap trafficCLIENT_TRAFFIC_SECRET_0
ServerApplicationTrafficSecrets ap trafficSERVER_TRAFFIC_SECRET_0
ExporterMasterSecretexp masterEXPORTER_SECRET
ResumptionMasterSecretres master不记
DerivedSecretderived不记

RFC 里还有 ext binder(外部 PSK 的绑定值)和 e exp master(早期导出密钥),rustls 0.23.46 的 SecretKind 里没有:它不支持外部 PSK,也不提供 0-RTT 阶段的密钥导出。e exp master 只出现在 KeySchedulePreHandshake 的文档注释画的阶梯图里。

流量密钥本身(AEAD 的 key 和 IV)不在日程里存,每次换密钥时从流量秘密现算:derive_traffic_key 用标签 key,derive_traffic_iv 用 iv,上下文都为空。KeyUpdate 用 traffic upd 从当前流量秘密派生下一个,票据 PSK 用 resumption 加票据 nonce。IV 怎么和序列号组成每条记录的 nonce,见加密与序列号。

转录哈希

每一步派生的「上下文」是一个转录哈希:到某条消息为止所有握手消息的哈希。哈希算法由套件决定,而套件要到 ServerHello 才知道,ClientHello 却早就发出去了。HandshakeHashBuffer 先把消息原样存进 Vec<u8>,start_hash 拿到哈希算法后把缓冲一次性灌进去,变成增量哈希 HandshakeHash:

pub(crate) struct HandshakeHash {
    provider: &'static dyn hash::Hash,
    ctx: Box<dyn hash::Context>,

    /// buffer for client-auth.
    client_auth: Option<Vec<u8>>,
}
  • 取值不结束哈希:current_hash 调 ctx.fork_finish(),在副本上结束,原哈希继续累加。一次握手要在好几个时刻取值。
  • 哈希的是原始编码:add_message 取 MessagePayload::Handshake { encoded, .. } 里收到或发出时的原始字节,不是把解析结果再编码一遍。
  • 客户端认证时保留全文:client_auth 只在可能要用全部握手消息时(TLS 1.2 的客户端认证签名的是全部消息,不是哈希)才保留一份,否则是 None。
  • HelloRetryRequest:into_hrr_buffer 把到 ClientHello1 为止的哈希包成一条合成的 message_hash 握手消息,作为新缓冲的开头,再接 HelloRetryRequest。这是 RFC 8446 §4.4.1 的规定,好让无状态的服务器只用 cookie 记住一个哈希值。

客户端在哪个时刻用哪个哈希:

用途转录范围代码
c hs traffic、s hs trafficClientHello … ServerHellohandle_server_hello 的 hash_at_client_recvd_server_hello
验服务器 CertificateVerify 签名ClientHello … CertificateExpectCertificateVerify
验服务器 FinishedClientHello … CertificateVerifyExpectFinished 开头
c ap traffic、s ap traffic、exp masterClientHello … 服务器 Finishedhash_after_handshake
客户端 FinishedClientHello … 服务器 Finished(有客户端证书时再加它们)flight.transcript.current_hash()
res masterClientHello … 客户端 Finishedinto_traffic 的参数
📝NOTE
冷知识:转录哈希曾经是「解析再重新编码」的结果

0.20.4 及以前,HandshakeHash::add_message 拿到的是解析后的消息,先调 get_encoding() 重新编码成字节再喂给哈希;自己发出的握手消息也要编码两次,一次进转录、一次上线。提交 86f4695c(「Use original handshake encoding for transcript hash」,首次发布在 0.20.5)让握手消息同时保存解析结果和原始编码,说明写的是「to avoid having to re-encode the message when updating the transcript hash. Also avoids encoding outgoing handshake message payloads twice.」就是今天 MessagePayload::Handshake { parsed, encoded } 这两个字段的来历。

对着测试向量核对

RFC 8448 的一步

RFC 8448 第 3 节给出了一次完整握手(套件 TLS_AES_128_GCM_SHA256)每一步的中间值。本机没有 RFC 文本。rustls 自己的 key_schedule.rs 测试不用 RFC 8448,注释写的是「These test vectors generated with OpenSSL」;仓库里搜不到 8448。本机能找到的出处是 Go 1.27.3 标准库的 src/crypto/tls/key_schedule_test.go:它的 TestTrafficKey 注明「tests derived from draft-ietf-tls-tls13-vectors-07」(RFC 8448 发布前的草案),给出服务端握手流量秘密(RFC 里标为 PRK)和由它展开出的握手写密钥、IV:

PRK (32 octets):  b6 7b 7d 69 0c c1 6c 4e 75 e5 42 13 cb 2d 37 b4
e9 c9 12 bc de d9 10 5d 42 be fd 59 d3 91 ad 38
key expanded (16 octets):  3f ce 51 60 09 c2 17 27 d0 f2 e4 e8 6e
e4 03 bc
iv expanded (12 octets):  5d 31 3e b2 67 12 76 ee 13 00 0b 30

这一步正是 rustls 每次换密钥时做的 HKDF-Expand-Label(secret, "key", "", 16) 和 HKDF-Expand-Label(secret, "iv", "", 12)。derive_traffic_key 和 derive_traffic_iv 是 pub fn,经 #[doc(hidden)] 的 rustls::internal 导出(不属于稳定接口),可以从外面直接调。实验程序 hsa-lab/src/bin/rfc8448.rs 用两个 provider 的 TLS13_AES_128_GCM_SHA256 各跑一次:

  let expander = suite.hkdf_provider.expander_for_okm(&OkmBlock::new(&prk));
  let key = rustls::internal::derive_traffic_key(expander.as_ref(), suite.aead_alg);
  let iv = rustls::internal::derive_traffic_iv(expander.as_ref());
[ring] key 3fce516009c21727d0f2e4e86ee403bc  match=true
[ring] iv  5d313eb2671276ee13000b30  match=true
[aws-lc-rs] key 3fce516009c21727d0f2e4e86ee403bc  match=true
[aws-lc-rs] iv  5d313eb2671276ee13000b30  match=true

反向对照:把 PRK 的第一个字节翻转一位(环境变量 FLIP=1),四项都变成 match=false(key ddf4ad8e…,iv 58df1993…),说明比较确实在起作用。

整条链:NIST ACVP 的向量

一步只验证了 HKDF-Expand-Label 的拼法。同一个 Go 测试文件里还有一组 NIST ACVP 的「TLS-v1.3-KDF-RFC8446」向量:给定 PSK、ECDHE 秘密和四个依次写进转录的随机值,期望各级秘密。这组向量能把 Extract、derived 盐和每个标签全走一遍。KeySchedule 是私有的,实验在打过补丁的副本里加了一个 hsa_chain 函数,按类型链的顺序直接调私有的 KeySchedule:

  let mut ks = KeySchedule::new(suite, psk);
  // c e traffic       <- hashes[0]
  ks.input_secret(dhe);
  // c hs / s hs       <- hashes[1]
  ks.input_empty();
  // c ap / s ap / exp <- hashes[2]
  // res master        <- hashes[3]

输出(每个秘密只打前 8 字节,match 是全长比较):

client_early_traffic_secret        3272189698C3594D  match=true
client_handshake_traffic_secret    B32306C3CE9932C4  match=true
server_handshake_traffic_secret    22787F8CA269D34B  match=true
client_application_traffic_secret  47D7EA08397B5871  match=true
server_application_traffic_secret  EFBDB0C873C0480D  match=true
exporter_master_secret             8A43D787EE3804EA  match=true
resumption_master_secret           5F4C961329C91044  match=true

七项全对。向量里的第八项 early_exporter_master_secret 没法核对,原因就是上面说的 SecretKind 里没有 e exp master。不依赖 rustls、自己用 HKDF 把 RFC 8448 第 3 节逐步算一遍,是动手:迷你密钥日程的内容。

❗IMPORTANT
动脑筋:给客户端和服务端各装一个只打印标签的 KeyLog,做一次完整握手(ring,不恢复会话),两边各打印几行?哪一边先打完?提示:服务端什么时候能算出应用流量密钥?答案在文末。

动脑筋答案

各 5 行,服务端先打完。实验程序 hsa-lab/src/bin/keylabels.rs 的输出:

server CLIENT_HANDSHAKE_TRAFFIC_SECRET (48 bytes)
server SERVER_HANDSHAKE_TRAFFIC_SECRET (48 bytes)
server CLIENT_TRAFFIC_SECRET_0 (48 bytes)
server SERVER_TRAFFIC_SECRET_0 (48 bytes)
server EXPORTER_SECRET (48 bytes)
client CLIENT_HANDSHAKE_TRAFFIC_SECRET (48 bytes)
client SERVER_HANDSHAKE_TRAFFIC_SECRET (48 bytes)
client CLIENT_TRAFFIC_SECRET_0 (48 bytes)
client SERVER_TRAFFIC_SECRET_0 (48 bytes)
client EXPORTER_SECRET (48 bytes)
suite Some(TLS13_AES_256_GCM_SHA384)
  • 5 行:derive_logged_secret 只在 SecretKind::log_label() 返回名字时写日志。没有 PSK 就没有 CLIENT_EARLY_TRAFFIC_SECRET;res master 和 derived 没有 KeyLog 名字,从不写出。
  • 48 字节:协商出的是 TLS13_AES_256_GCM_SHA384,秘密长度等于 SHA-384 的输出。
  • 服务端先打完:服务端在处理 ClientHello 的那一次 handle 里就发出了 ServerHello 到 Finished 的整个第一轮,同时经 into_traffic_with_client_finished_pending 派生了应用流量密钥,5 行一次写完;客户端要到第一轮回来、收到 ServerHello 才写出前 2 行,处理完服务器 Finished 才写出后 3 行。

小结

  • RFC 8446 §7.1 的三级 Extract 在代码里是 KeyScheduleEarly / KeySchedulePreHandshake → KeyScheduleHandshakeStart → KeyScheduleHandshake → KeyScheduleClientBeforeFinished 或 KeyScheduleTrafficWithClientFinishedPending → KeyScheduleTraffic + KeyScheduleResumption。
  • 每一步消耗 self;跳步、重复派生、在错的阶段算 Finished 都是编译错误(E0599、E0382),实验在 crate 内部复现了三条原文报错。类型管不到传入的哈希是否正确。
  • 底下是一个私有的 KeySchedule,input_secret / input_empty 用 derived 盐进下一级,hkdf_expand_label_inner 拼 tls13 前缀的标签;ext binder 和 e exp master 不支持。
  • 转录哈希先缓冲、在 ServerHello 定下套件后变增量哈希,fork_finish 取中间值,哈希原始编码(0.20.5 起),HelloRetryRequest 时折成 message_hash。
  • 握手写密钥和 IV 与 draft-ietf-tls-tls13-vectors-07(经 Go 标准库测试转载)一致,两个 provider 都对;NIST ACVP 向量的七个秘密经私有 KeySchedule 全部一致。

💬 关联讨论

发起讨论
加载中...