TLS 1.3规范及0-RTT原理

前言

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

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

考虑session重用,根据数据表明,大部分的TLS的请求都在重用, TLS1.2 session重用需要1个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时间?

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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发送

下面是TLS 1.3的握手流程来自TLS 1.3规范, 截止本文尚处在draft-20状态

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
Client Server
Key ^ ClientHello
Exch | + key_share*
| + signature_algorithms*
| + psk_key_exchange_modes*
v + pre_shared_key* -------->
ServerHello ^ Key
+ key_share* | Exch
+ pre_shared_key* v
{EncryptedExtensions} ^ Server
{CertificateRequest*} v Params
{Certificate*} ^
{CertificateVerify*} | Auth
{Finished} v
<-------- [Application Data*]
^ {Certificate*}
Auth | {CertificateVerify*}
v {Finished} -------->
[Application Data] <-------> [Application Data]
+ Indicates noteworthy extensions sent in the
previously noted message.
* Indicates optional or situation-dependent
messages/extensions that are not always sent.
{} Indicates messages protected using keys
derived from a [sender]_handshake_traffic_secret.
[] Indicates messages protected using keys
derived from [sender]_application_traffic_secret_N
Figure 1: Message flow for full TLS Handshake

后文所有的代码来源于下面两个库,
server端源码来自cloudflare的tls-tris: https://github.com/cloudflare/tls-tris
因为tls-tris截止目前没有实现tls 1.3的客户端,也没有实现双向验证, client端参考了https://github.com/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
19
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 & 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 secret导出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
12
13
14
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,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
9
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
65
66
67
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
14
15
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
7
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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
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

参考

https://tlswg.github.io/tls13-spec/