写一篇关于 TLS 1.3 的文章


首先需要明确的是,同等情况下,TLS1.3 比 1.2 少一个 RTT 时间。

客户端完成 TCP 握手需要一个 RTT 时间, TLS1.2 完成TLS 密钥协商需要两个 RTT 时间, TLS1.3 只需要一个 RTT 时间。
因此对于 https, 收到第一个 http 响应包,TLS1.2 需要 4 个 RTT 时间, TLS1.3 需要 3 个 RTT 时间。

考虑 session 重用,根据数据表明,大部分的 TLS 的请求都在重用, TLS1.2 session 重用需要一个 RTT 时间, TLS1.3 则因为在第一个包中携带数据,只需要0个 RTT,有点类似 TLS 层的TCP Fast Open。
因此对于 https, 收到第一个 http 响应包,比非重用减少一个 RTT, TLS1.2 需要3个 RTT 时间, TLS1.3 需要2个 RTT 时间。

另外如果开启 TCP 的 TFO,收到第一个 https 响应包的时间,则再减少一个 RTT,在 session 重用的时候就是 TLS1.2 需要2个 RTT,TLS1.3 只需要1个 RTT 时间。

How

为什么 TLS 1.3 能少一个 RTT 时间?

考虑 TLS 1.2, 下面握手流程来自 RFC5246 ,在第一个 RTT 需要协商算法版本等信息, 在第二个 RTT 才能完成对称密钥的协商。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Client                                        Server
ClientHello -------->
ServerHello
Certificate*
ServerKeyExchange*
CertificateRequest*
<-------- ServerHelloDone
Certificate*
ClientKeyExchange
CertificateVerify*
[ChangeCipherSpec]
Finished -------->
[ChangeCipherSpec]
<-------- Finished
Application Data <-------> Application Data

Figure 1. Message flow for a full handshake

如果要减少 RTT,就是解决如何在第一个 RTT 内就能完成密钥的协商?
因为 TLS 1.3 只支持 PFS 的算法,已经取消了 RSA 作为密钥协商算法, 因此以下讨论仅用 ECDHE 来说明。

使用 ECDHE 需要解决以下问题:

  • 双方把自己的公钥发送给对方
  • 确认发送的公钥信息没有被中间人篡改

很自然的,我们会考虑能不能把 ECDHE 的公钥在 client hello 中发送?
服务端收到 client hello 后,随机生成本地的 ECDHE 私钥后,就能直接能计算出 pre-master secret,进而计算出所有密钥。
同时服务端发送 finish 消息,通过 hmac 验证,client hello 中的 ECDHE 公钥没有被篡改。

在第二个 RTT 开始,收到 server hello 后,client 也能通过服务端 ECDHE 的公钥计算出 pre-master secret,
发送自己的 Finish 消息,并和应用数据一起发送。 服务端验证 Finish 成功后才接收数据。

上面分析,完全是拍脑袋的结果,事实上 TLS 1.3 是这样吗?

TLS 1.3

对比TLS 1.2主要的修改如下:

  • 使用更严格的算法,只使用 PFS 的算法,如禁用了 RSA 密钥协商, 只使用 AEAD 算法
  • 使用 HKDF 密钥导出算法替代 PRF 算法
  • server hello 之后的握手包也开始加密, 并去掉了changeCipherSpec 消息
  • 更改了 session 重用机制, 使用 PSK 的机制,同时 session ticket 中添加了过期时间。 过去 TLS 1.2 中的 ticket 不包含过期时间,只能通过 ticket key 的更新让之前所有发送的 ticket 都失效
  • 版本协商作为 client hello 的扩展,提供版本列表
  • 支持 0-RTT 发送

后文所有的代码来源于下面两个库,
server 端源码来自 Cloudflare 的 tls-tris,Client 端参考了 bifurcation/mint

clientHello

如前文所述, TLS 1.3 为了减少一个 RTT 时间,必须在 client hello 中发送本地的 ECDHE 的公钥,因为可能支持多个曲线,所以需要发送每个曲线的 ECDHE 公钥。
每个公钥和对应的曲线, 称为 keyShare。 keyshare 列表作为 clientHello 的扩展被发送

1
2
3
4
5
// 每个 keyShare 的条目,包含曲线 ID 和公钥
struct {
NamedGroup group;
opaque key_exchange<1..2^16-1>;
} KeyShareEntry;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func (h *ClientHandshake) CreateClientHello(opts ConnectionOptions, caps Capabilities) (*HandshakeMessage, error) {
// key_shares
h.OfferedDH = map[NamedGroup][]byte{}
ks := KeyShareExtension{
HandshakeType: HandshakeTypeClientHello,
Shares: make([]KeyShareEntry, len(caps.Groups)),
}
for i, group := range caps.Groups {
pub, priv, err := newKeyShare(group) //为每个支持的曲线,生成公私钥,作为keyshare
if err != nil {
return nil, err
}
ks.Shares[i].Group = group
ks.Shares[i].KeyExchange = pub
h.OfferedDH[group] = priv
}
...
}

serverHello

and HelloRetryRequest, EncryptedExtensions, CertificateRequest, Certifacate, CertificateVerify, Finished

  • 服务端收到 client 后,协商曲线,如果有支持的曲线则使用该 keyshare, 否则发送 HelloRetryRequest 消息通知client。
  • 服务端生成 ECDHE 公私钥后, 通过客户端的 keyShare 协商出密钥 ECDHESecret(TLS 1.2 中的 premaster secret)。然后通过 serverHello 发送服务端的 keyShare。
    需要注意的是 keyShare 没有使用私钥签名, 整个过程的不可抵赖和防篡改是通过 certificateVerify 证明持有私钥,以及 finished 消息使用 hmac 验证历史消息来确定的。
  • serverHello 之后的握手消息需要加密,导出加密密钥。
    通过 early secret 和 ECDHE secre t导出server_handshake_traffic_secret。
    再从 server_handshake_traffic_secret 中导出 key 和 iv,使用该 key 和 iv 对 server hello 之后的握手消息加密。
    同样的计算 client_handshake_traffic_secret,使用对应的 key 和 iv 进行解密后续的握手消息
1
2
3
4
5
6
7
8
9
10
11
Derive-Secret(Secret, Label, Messages) =
HKDF-Expand-Label(Secret, Label,
Transcript-Hash(Messages), Hash.length)
early secret=HKDF-Extract(0,0)
Handshake Secret = HKDF-Extract(ecdhe secret, early secret)
server_handshake_traffic_secret = Derive-Secret(Handshake Secret, "server handshake traffic secret", ClientHello...ServerHello)
client_handshake_traffic_secret = Derive-Secret(Handshake Secret, "server handshake traffic secret", ClientHello...ServerHello)
server_handshake_key := hkdfExpandLabel(hash, server_handshake_traffic_secret, nil, "key", hs.suite.keyLen)
server_handshake_iv := hkdfExpandLabel(hash, server_handshake_traffic_secret, nil, "iv", 12)
client_handshake_key := hkdfExpandLabel(hash, server_handshake_traffic_secret, nil, "key", hs.suite.keyLen)
client_handshake_iv := hkdfExpandLabel(hash, server_handshake_traffic_secret, nil, "iv", 12)
  • 在 EncryptedExtensions 消息中发送扩展信息,比如 alpn 协议,服务端是否支持 earlyData
  • 如果服务端需要客户端证书,则发送 CertificateRequest , 在其扩展中指定支持的签名算法和CA
  • 发送 certificate 和 certificateVerify 消息
    在 certificate 可以指定 OCSP stapling和 sct。
    certificateVerify 跟以前 client 发送的类似, 使用私钥对历史握手消息的摘要进行签名, 并发送签名的算法。
  • 发送 finished 消息, 从 server_handshake_traffic_secret 中导出 serverFinishedKey, 使用 hmac 计算 finished 后发送。 TLS 1.2 是使用 PRF(master_secret, digest(handshake)) 导出的。
  • 导出最终的对称密钥。 先从 Handshake Secret 中导出 master secret,再从 master secret 导出两个方向的对称密钥 key 和 iv
1
2
3
masterSecret = hkdfExtract(hash, nil, Handshake Secret)
client_application_traffic_secret_0 = Derive-Secret(masterSecret, "client application traffic secret", ClientHello...server Finished)
server_application_traffic_secret_0 = Derive-Secret(masterSecret, "server application traffic secret", ClientHello...server Finished)

Certifacate

and CertificateVerify, Finished

客户端收到 serverHello 后,通过 server 的 keyshare 计算出 ECDHE secret。
然后跟 server 端一样,通过一系列的 khdf 密钥导出, 两个方向的后续握手密钥,以及 master secret 和两个方向的 application traffic secret。
因为 client 和 server 端 early secret 和协商出来的 ecdhe secret 相同, 因此所有后续导出的对应的密钥都是相同的。

1
2
3
4
5
6
7
8
early secret=HKDF-Extract(0,0)
Handshake Secret = HKDF-Extract(ecdhe secret, early secret)
server_handshake_traffic_secret = Derive-Secret(Handshake Secret, "server handshake traffic secret", ClientHello...ServerHello)
client_handshake_traffic_secret = Derive-Secret(Handshake Secret, "client handshake traffic secret", ClientHello...ServerHello)
masterSecret = hkdfExtract(hash, nil, Handshake Secret)
client_application_traffic_secret_0 = Derive-Secret(masterSecret, "client application traffic secret", ClientHello...server Finished)
server_application_traffic_secret_0 = Derive-Secret(masterSecret, "server application traffic secret", ClientHello...server Finished)
resumption_master_secret = Derive-Secret(masterSecret, "resumption master secret", hash(all handshake message))

发送 finished 后, 就完成了整个握手信息, 通过 master secret 和整个握手的摘要,计算 resumption secret

newSessionTicket

  • 收到客户端的 Certifacate 和 CertificateVerify,同样进行证书链的认证以及验证签名
  • 服务端收到客户端的 finished 消息后,验证完后,同样计算 resumption secret
  • 发送 new session ticket,包含整个 session 的信息。 newSessionTicket 使用server_application_traffic_secret 加密
    在加密的ticket中,相比 TLS1.2,包含了当前的创建时间,因此可以方便的配置和验证 ticket 的过期时间。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
func (hs *serverHandshakeState) sendSessionTicket13() error {
c := hs.c
if c.config.SessionTicketsDisabled {
return nil
}
foundDHE := false
for _, mode := range hs.clientHello.pskKeyExchangeModes {
if mode == pskDHEKeyExchange {
foundDHE = true
break
}
}
if !foundDHE {
return nil
}
//只支持dhe的方式计算psk
hash := hashForSuite(hs.suite)
handshakeCtx := hs.finishedHash13.Sum(nil)
resumptionSecret := hkdfExpandLabel(hash, hs.masterSecret, handshakeCtx, "resumption master secret", hash.Size())
ageAddBuf := make([]byte, 4)
sessionState := &sessionState13{ //需要加密的session信息,包含resumptionSecret
vers: c.vers,
suite: hs.suite.id,
createdAt: uint64(time.Now().Unix()),
resumptionSecret: resumptionSecret,
alpnProtocol: c.clientProtocol,
SNI: c.serverName,
maxEarlyDataLen: c.config.Max0RTTDataSize,
}
for i := 0; i < numSessionTickets; i++ {
if _, err := io.ReadFull(c.config.rand(), ageAddBuf); err != nil { //随机生成ageAddBuf
c.sendAlert(alertInternalError)
return err
}
sessionState.ageAdd = uint32(ageAddBuf[0])<<24 | uint32(ageAddBuf[1])<<16 | //ageAdd使用随机值
uint32(ageAddBuf[2])<<8 | uint32(ageAddBuf[3])
ticket := sessionState.marshal()
var err error
if c.config.SessionTicketSealer != nil {
cs := c.ConnectionState()
ticket, err = c.config.SessionTicketSealer.Seal(&cs, ticket)
} else {
ticket, err = c.encryptTicket(ticket) //使用tiket key加密
}
if err != nil {
c.sendAlert(alertInternalError)
return err
}
if ticket == nil {
continue
}
ticketMsg := &newSessionTicketMsg13{
lifetime: 24 * 3600, // TODO(filippo) //24小时
maxEarlyDataLength: c.config.Max0RTTDataSize,
withEarlyDataInfo: c.config.Max0RTTDataSize > 0,
ageAdd: sessionState.ageAdd, //随机值
ticket: ticket, //session信息
}
if _, err := c.writeRecord(recordTypeHandshake, ticketMsg.marshal()); err != nil {
return err
}
}
return nil
}

session 重用和 0-RTT

client 收到 NewSessionTicket 消息后,
收到的 ticket 和客户端本地发送 finished 后计算的 resumptionSecret,两者一起组成了 PreSharedKey,即 PSK。
然后 client 把 PSK 保存到本地 cache 中, serverName 作为 cache 的 key。

1
2
3
4
5
6
7
8
9
10
11
12
13
func (h *ClientHandshake) HandleNewSessionTicket(hm *HandshakeMessage) (PreSharedKey, error) {
var tkt NewSessionTicketBody
_, err := tkt.Unmarshal(hm.body)

psk := PreSharedKey{
CipherSuite: h.Context.suite,
IsResumption: true,
Identity: tkt.Ticket, // ticket 中也包含 resumptionSecret,但是被加密
Key: h.Context.resumptionSecret, //客户端本地发送 finished 后计算的 resumptionSecret
}
return psk, nil
}
c.config.PSKs.Put(c.config.ServerName, psk) //这里使用 serverName 做为 key

client

在 client hello 中,会在本地 cache 中查找 servername 对应的 PSK, 找到后则在 client hello 的 psk 扩展中带上两部分

  • Identity: 就是 NewSessionTicket 中加密的 ticket
  • Binder: 从之前 client 发送 finished 计算的 resumption secret,导出 early secret,进而导出后续的 binderKey 和 binder_macKey, 使用 binder_macKey 对不包含 PSK 部分的 clientHello 作HMAC
1
2
3
4
5
6
Early Secret = HKDF-Extract(0, resumption secret)
binder_key = deriveSecret(Early Secret, "resumption psk binder key", "")
binder_macKey = hkdfExpandLabel(ctx.params.hash, binder_key, "finished", []byte{}, ctx.params.hash.Size())
earlyTrafficSecret = ctx.deriveSecret(Early Secret, "client early traffic secret", clientHello)
earlyExporterSecret = ctx.deriveSecret(Early Secret, "early exporter master secret", ClientHello)
clientEarlyTrafficKey, clientEarlyTrafficIv= ctx.makeTrafficKeys(earlyTrafficSecret)

通过 resumption secret 最终导出 earlyData 的加密密钥,以及 PSK 扩展中 binder 的 hmac 密钥。
发送 clientHello 后,使用 resumption secret 导出的 clientEarlyTrafficKey 和 IV,对 early data 加密后发送。

需要注意的是 earlydata 在 ticket 有效期内,不能防止重放攻击。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
func (h *ClientHandshake) CreateClientHello(opts ConnectionOptions, caps Capabilities) (*HandshakeMessage, error) {
// key_shares
h.OfferedDH = map[NamedGroup][]byte{}
ks := KeyShareExtension{
HandshakeType: HandshakeTypeClientHello,
Shares: make([]KeyShareEntry, len(caps.Groups)),
}
for i, group := range caps.Groups {
pub, priv, err := newKeyShare(group) //为每个支持的曲线,生成公私钥,作为keyshare
if err != nil {
return nil, err
}
ks.Shares[i].Group = group
ks.Shares[i].KeyExchange = pub
h.OfferedDH[group] = priv
}
...
if key, ok := caps.PSKs.Get(opts.ServerName); ok { //从cache中获取PSK,尝试session重用
h.OfferedPSK = key
keyParams, ok := cipherSuiteMap[key.CipherSuite]
compatibleSuites := []CipherSuite{}
for _, suite := range ch.CipherSuites {
if cipherSuiteMap[suite].hash == keyParams.hash {
compatibleSuites = append(compatibleSuites, suite)
}
}
ch.CipherSuites = compatibleSuites //更新psk能使用的算法
if opts.EarlyData != nil { //使用psk的话可以使用0-rtt发送early data
ed = &EarlyDataExtension{} //开启early data
ch.Extensions.Add(ed)
}

psk = &PreSharedKeyExtension{
HandshakeType: HandshakeTypeClientHello,
Identities: []PSKIdentity{
{Identity: key.Identity}, //Identity就是加密的ticket
},
Binders: []PSKBinderEntry{
// Note: Stub to get the length fields right
{Binder: bytes.Repeat([]byte{0x00}, keyParams.hash.Size())},
},
}
ch.Extensions.Add(psk) //添加psk作为client hello的扩展
h.Context.preInit(key) //从resumption secret导出early secret->binder key
trunc, err := ch.Truncated() //clientHello减去psk 扩展部分
truncHash := h.Context.params.hash.New()
truncHash.Write(trunc)
binder := h.Context.computeFinishedData(h.Context.binderKey, truncHash.Sum(nil)) //binder_key导出macKey, 计算clientHello hmac
// Replace the PSK extension
psk.Binders[0].Binder = binder //client hello的hmac
ch.Extensions.Add(psk) //替换psk扩展
h.clientHello, err = HandshakeMessageFromBody(ch) //重新构造client hello

h.Context.earlyUpdateWithClientHello(h.clientHello) //导出client_early_traffic_secret及其key和iv,作为0-RTT的early data的密钥
}
...
}
func (c *Conn) clientHandshake() error {
logf(logTypeHandshake, "Starting clientHandshake")
h := &ClientHandshake{}
hIn := NewHandshakeLayer(c.in)
hOut := NewHandshakeLayer(c.out)
// Generate ClientHello
caps := Capabilities{
CipherSuites: c.config.CipherSuites,
Groups: c.config.Groups,
SignatureSchemes: c.config.SignatureSchemes,
PSKs: c.config.PSKs,
PSKModes: c.config.PSKModes,
Certificates: c.config.Certificates,
}
opts := ConnectionOptions{
ServerName: c.config.ServerName,
NextProtos: c.config.NextProtos,
EarlyData: c.earlyData,
}
chm, err := h.CreateClientHello(opts, caps)
if err != nil {
return err
}
// Write ClientHello
err = hOut.WriteMessage(chm)
if err != nil {
return err
}
if opts.EarlyData != nil { //使用client_early_traffic_secret的key/iv加密early data, 支持0-rtt发送
// Rekey output to early data keys
err = c.out.Rekey(h.Context.params.cipher, h.Context.clientEarlyTrafficKeys.key, h.Context.clientEarlyTrafficKeys.iv)
// Send early application data
logf(logTypeHandshake, "[client] Sending data...")
_, err = c.Write(opts.EarlyData)
if err != nil {
return err
}
// Send end_of_earlyData
logf(logTypeHandshake, "[client] Sending end_of_early_data...")
err = c.sendAlert(AlertEndOfEarlyData) //发送end_of_early_data alert标记early data结束
if err != nil {
return err
}
}
...
}

server

和 TLS 1.2 之前不同, session 重用,使用的不是过去的 master secret。
TLS1.2 加密 ticket 后使用过去的 master secret,然后和两个随机数作为参数,一起 PRF 导出密钥。
而 TLS 1.3 只使用过去的 resumption secret 导出 early data 的密钥, 之后的密钥会和 ECDHE secret,一起导出。

  • 服务端收到 client hello 后,生成本地的 keyShare
  • 检查 client hello 的 PSK 扩展, 解密 ticket,查看该 ticket 是否过期,已经版本算法等协商结果是否可用,然后使用 ticket 中的 resumption secret 计算 client hello 的 hmac, 检查 binder 是否正确。
  • 验证完 ticket 和 binder 之后,在 serverHello 中表示使用 PSK,以及哪个 PSK。
  • 和 client 一样,从 resumtion secret 中导出 earlyData 使用的密钥
  • 和不使用 session 重用一样,导出后续的密钥,唯一不同的是 resumption secret 作为 early secret 的输入
  • 收到 endOfEarlyData alert 后,切换到 client 方向的应用程序密钥
  • serverHello 发送后依然会发送 EncryptedExtensions 和 Finished 消息,但不会再发送 Certificate 和 CerficateVerify 消息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
func (hs *serverHandshakeState) doTLS13Handshake() error {

var ks keyShare
CurvePreferenceLoop:
for _, curveID := range config.curvePreferences() { // tls 1.3所有的曲线,都必须生成对应的keyshare, keyshare中包含dh的公钥
for _, keyShare := range hs.clientHello.keyShares {
if curveID == keyShare.group {
ks = keyShare
break CurvePreferenceLoop
}
}
}

privateKey, serverKS, err := config.generateKeyShare(ks.group) //ecdhe服务端生成公私钥
hs.hello13.keyShare = serverKS //公钥作为keyshare
earlySecret, pskAlert := hs.checkPSK() // 检查psk,看能否session重用
switch {
...
case earlySecret != nil:
c.didResume = true //ticket和binder验证通过。
}
hs.finishedHash13 = hash.New()
hs.finishedHash13.Write(hs.clientHello.marshal())
handshakeCtx := hs.finishedHash13.Sum(nil) //client hello摘要
earlyClientCipher, _ := hs.prepareCipher(handshakeCtx, earlySecret, "client early traffic secret") //client_early_traffic_secret=Derive-Secret(earlySecret, "client early traffic secret",hash(clientHello))
ecdheSecret := deriveECDHESecret(ks, privateKey) //客户端公钥和server私钥,根据曲线乘法,计算出对称密钥

hs.finishedHash13.Write(hs.hello13.marshal())
//发送server hello, 包含协商信息, 需要注意的是,服务端的keyshare没有使用私钥签名
if _, err := c.writeRecord(recordTypeHandshake, hs.hello13.marshal()); err != nil {
return err
}
// 和不使用session重用一致,只是early secret使用resumption secret作为输入
// 不再发送发送Certificate和CerficateVerify消息
...
return nil
}
func (hs *serverHandshakeState) checkPSK() (earlySecret []byte, alert alert) {
if hs.c.config.SessionTicketsDisabled {
return nil, alertSuccess
}
foundDHE := false
for _, mode := range hs.clientHello.pskKeyExchangeModes {
if mode == pskDHEKeyExchange { //只支持psk dhe模式
foundDHE = true
break
}
}
if !foundDHE {
return nil, alertSuccess
}
hash := hashForSuite(hs.suite)
hashSize := hash.Size()
for i := range hs.clientHello.psks {
sessionTicket := append([]uint8{}, hs.clientHello.psks[i].identity...)
if hs.c.config.SessionTicketSealer != nil {
var ok bool
sessionTicket, ok = hs.c.config.SessionTicketSealer.Unseal(hs.clientHelloInfo(), sessionTicket)
if !ok {
continue
}
} else {
sessionTicket, _ = hs.c.decryptTicket(sessionTicket) //使用默认的session ticket key解密
if sessionTicket == nil {
continue
}
}
s := &sessionState13{} //还原tls 1.3 session
if s.unmarshal(sessionTicket) != alertSuccess {
continue
}
if s.vers != hs.c.vers {
continue
}
//client收到ticket后,通过lifetime,计算obfTicketAge,并加上随机值ageAdd,这里减回去,得到client的ticket有效时间
clientAge := time.Duration(hs.clientHello.psks[i].obfTicketAge-s.ageAdd) * time.Millisecond //tls 1.3 ticket带时间了
serverAge := time.Since(time.Unix(int64(s.createdAt), 0)) //距离本次ticket的创建时间
if clientAge-serverAge > ticketAgeSkewAllowance || clientAge-serverAge < -ticketAgeSkewAllowance {
// XXX: NSS is off spec and sends obfuscated_ticket_age as seconds
clientAge = time.Duration(hs.clientHello.psks[i].obfTicketAge-s.ageAdd) * time.Second
if clientAge-serverAge > ticketAgeSkewAllowance || clientAge-serverAge < -ticketAgeSkewAllowance {
continue
}
}
// This enforces the stricter 0-RTT requirements on all ticket uses.
// The benefit of using PSK+ECDHE without 0-RTT are small enough that
// we can give them up in the edge case of changed suite or ALPN or SNI.
if s.suite != hs.suite.id {
continue
}
if s.alpnProtocol != hs.c.clientProtocol {
continue
}
if s.SNI != hs.c.serverName {
continue
}
earlySecret := hkdfExtract(hash, s.resumptionSecret, nil) // earlySecret = hkdfExtract(psk, 0); psk=resumption_master_secret
handshakeCtx := hash.New().Sum(nil)
binderKey := hkdfExpandLabel(hash, earlySecret, handshakeCtx, "resumption psk binder key", hashSize) //binder_key=Derive-Secret(early secret, "resumption psk binder key", "")
binderFinishedKey := hkdfExpandLabel(hash, binderKey, nil, "finished", hashSize) //finished_key=Derive-Secret(binder_key, "finished", "")
chHash := hash.New()
chHash.Write(hs.clientHello.rawTruncated) //不包含psk扩展
expectedBinder := hmacOfSum(hash, chHash, binderFinishedKey) //通过finishKey计算clienthello的hmac
if subtle.ConstantTimeCompare(expectedBinder, hs.clientHello.psks[i].binder) != 1 { //hmac验证
return nil, alertDecryptError
}
if i == 0 && hs.clientHello.earlyData {
// This is a ticket intended to be used for 0-RTT
if s.maxEarlyDataLen == 0 {
// But we had not tagged it as such.
return nil, alertIllegalParameter
}
if hs.c.config.Accept0RTTData { //服务端支持0rtt, 0rtt会引起重放攻击
hs.c.binder = expectedBinder
hs.c.ticketMaxEarlyData = int64(s.maxEarlyDataLen)
hs.hello13Enc.earlyData = true
}
}
hs.hello13.psk = true
hs.hello13.pskIdentity = uint16(i)
return earlySecret, alertSuccess
}
return nil, alertSuccess
}
func (c *Conn) handleEndOfEarlyData() {
if c.phase != readingEarlyData || c.vers < VersionTLS13 {
c.in.setErrorLocked(c.sendAlert(alertUnexpectedMessage))
return
}
c.phase = waitingClientFinished
if c.hand.Len() > 0 {
c.in.setErrorLocked(c.sendAlert(alertUnexpectedMessage))
return
}
c.in.setCipher(c.vers, c.hs.hsClientCipher) //切换client的应用程序密钥
}

记录层

通过clientHello中带有short headers扩展, 删除了记录层开头的几个字节

HKDF

(HMAC-based key derivation function)

HKDF是基于HMAC的密钥导出算法,用来替换TLS 1.3之前的PRF算法。

HKDF follows the “extract-then-expand” paradigm, where the KDF
logically consists of two modules. The first stage takes the input
keying material and “extracts” from it a fixed-length pseudorandom
key K. The second stage “expands” the key K into several additional
pseudorandom keys (the output of the KDF).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
0
|
v
PSK(resumption secret) -> HKDF-Extract = Early Secret
|
+-----> Derive-Secret(.,
| "external psk binder key" |
| "resumption psk binder key",
| "")
| = binder_key
|
+-----> Derive-Secret(., "client early traffic secret",
| ClientHello)
| = client_early_traffic_secret
|
+-----> Derive-Secret(., "early exporter master secret",
| ClientHello)
| = early_exporter_master_secret
v
Derive-Secret(., "derived secret", "")
|
v
(EC)DHE -> HKDF-Extract = Handshake Secret
|
+-----> Derive-Secret(., "client handshake traffic secret",
| ClientHello...ServerHello)
| = client_handshake_traffic_secret
|
+-----> Derive-Secret(., "server handshake traffic secret",
| ClientHello...ServerHello)
| = server_handshake_traffic_secret
v
Derive-Secret(., "derived secret", "")
|
v
0 -> HKDF-Extract = Master Secret
|
+-----> Derive-Secret(., "client application traffic secret",
| ClientHello...server Finished)
| = client_application_traffic_secret_0
|
+-----> Derive-Secret(., "server application traffic secret",
| ClientHello...server Finished)
| = server_application_traffic_secret_0
|
+-----> Derive-Secret(., "exporter master secret",
| ClientHello...server Finished)
| = exporter_master_secret
|
+-----> Derive-Secret(., "resumption master secret",
ClientHello...client Finished)
= resumption_master_secret