密钥日程
主线位置:握手主线里和状态并排走的另一条链。客户端收到 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) | KeyScheduleEarly | new(suite, psk) | 算 PSK 绑定值、client_early_traffic_secret |
| Early Secret(待输入 ECDHE) | KeySchedulePreHandshake | new(suite)(无 PSK)或 From<KeyScheduleEarly> | 只能 into_handshake |
| Handshake Secret(刚算出) | KeyScheduleHandshakeStart | into_handshake(shared_secret) | 派生两个握手流量密钥(分客户端、服务端两个方法),ECH 确认 |
| Handshake Secret(密钥已派生) | KeyScheduleHandshake | derive_*_handshake_secrets | 算服务器 Finished,换握手加密 / 解密器 |
| Master Secret,客户端 Finished 未发 | KeyScheduleClientBeforeFinished | into_pre_finished_client_traffic | 应用密钥已派生,但还没装进记录层 |
| Master Secret,等客户端 Finished | KeyScheduleTrafficWithClientFinishedPending | into_traffic_with_client_finished_pending(服务端) | 已用应用密钥加密,验客户端 Finished |
| 应用阶段 | KeyScheduleTraffic | into_traffic | KeyUpdate、导出密钥材料、取出流量密钥 |
| 恢复 | 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 才一次装上两个方向。
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 名字 |
|---|---|---|
ResumptionPskBinderKey | res binder | 不记 |
ClientEarlyTrafficSecret | c e traffic | CLIENT_EARLY_TRAFFIC_SECRET |
ClientHandshakeTrafficSecret | c hs traffic | CLIENT_HANDSHAKE_TRAFFIC_SECRET |
ServerHandshakeTrafficSecret | s hs traffic | SERVER_HANDSHAKE_TRAFFIC_SECRET |
ClientApplicationTrafficSecret | c ap traffic | CLIENT_TRAFFIC_SECRET_0 |
ServerApplicationTrafficSecret | s ap traffic | SERVER_TRAFFIC_SECRET_0 |
ExporterMasterSecret | exp master | EXPORTER_SECRET |
ResumptionMasterSecret | res master | 不记 |
DerivedSecret | derived | 不记 |
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 traffic | ClientHello … ServerHello | handle_server_hello 的 hash_at_client_recvd_server_hello |
| 验服务器 CertificateVerify 签名 | ClientHello … Certificate | ExpectCertificateVerify |
| 验服务器 Finished | ClientHello … CertificateVerify | ExpectFinished 开头 |
c ap traffic、s ap traffic、exp master | ClientHello … 服务器 Finished | hash_after_handshake |
| 客户端 Finished | ClientHello … 服务器 Finished(有客户端证书时再加它们) | flight.transcript.current_hash() |
res master | ClientHello … 客户端 Finished | into_traffic 的参数 |
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 节逐步算一遍,是动手:迷你密钥日程的内容。
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全部一致。