深入TLS实现: golang TLS分析

前言

因为最近要搞个东西对TLS有定制化需求, 修改openssl有较大的复杂度,同时考虑golang对很多第三方库接入的友好,以及goroutine编写应用的便利,所以考虑了下使用golang。
通过阅读golang TLS,以及对比openssl,很明显可以感觉使用协程通过同步的方式编成带来的便利。
通过协程,完全是一个逻辑走到底,整个流程的处理非常清晰,避免了使用各种callback和异步处理带来的复杂度。
好遗憾以前学openssl的没有先看golang的实现。

不过,golang TLS也有一些缺点, 比如没有官方对DTLS的实现,某些扩展支持不好或不支持,

本文通过golang TLS的实现来分析TLS协议,对于整个TLS的过程和原理有个全局的认识。

RSA & ECC

RSA既能用来做非对称加密, 也能用来做数字签名。

对于相同安全性,ECC需要的密钥长度比RSA短。
下面的长度效率对比数据来自rfc4492

1
2
3
4
5
6
7
8
9
Symmetric | ECC | DH/DSA/RSA
------------+---------+-------------
80 | 163 | 1024
112 | 233 | 2048
128 | 283 | 3072
192 | 409 | 7680
256 | 571 | 15360
Table 1: Comparable Key Sizes (in bits)

ECC椭圆曲线算法是通过将椭圆曲线上特定的点进行特殊的乘法运送来实现的,它利用了这种乘法运算的逆运算非常困难的特性。

内置ECDSA公钥的证书一般被称之为ECC证书,内置RSA公钥的证书就是RSA证书。
ECDHE是密钥协商算法, 它需要使用私钥对其发送的参数进行签名, 而签名既可以使用rsa也可以使用ecdsa

简单的说签名就是认证,rsa的证书只能用rsa来认证,ecc证书只能用ecdsa认证。 签名验签主要为了确认对方确实拥有对应的私钥,以提供不可抵赖的证明。

RSA也能作密钥协商算法,但是在TLS1.3中已经被禁用, 所以目前密钥协商算法尽量还是用ECDHE算法。

Client Hello

Client Hello是第一个握手包,带上client本地的信息发送给server。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type clientHelloMsg struct {
raw []byte
vers uint16 //版本
random []byte //客户端随机数
sessionId []byte //session重用的时候,带上sessionTicket后,随机生成一个sessionId
cipherSuites []uint16 //客户端支持的算法套件
compressionMethods []uint8 //压缩算法,默认不使用,客户端必须发送null算法
nextProtoNeg bool //支持NPN, NPN是服务端发送应用协议列表。 开启后,当没有发送alpn的时候,如果服务端指定了应用协议,则sever hello的时候会返回
serverName string //比如http请求的域名
ocspStapling bool //是否让服务端尝试返回ocsp stapling
scts bool //是否支持scts, signed certificate timestamp support, scts是certificate transparency的一部分,可以避免ca滥发/误发证书,提供公开审计,让ca/域名拥有者/用户能检查到域名是否被恶意多次签发
supportedCurves []CurveID //支持的ecc曲线
supportedPoints []uint8 //支持的点的格式,如果发送必须包含0(uncompressed)
ticketSupported bool //是否支持ticket, 客户端如果支持session cache就支持ticket
sessionTicket []uint8 //如果session重用,则带上sessionTicket发送给服务端
signatureAndHashes []signatureAndHash //客户端支持的签发算法, 在tls1.2以上被server在serverkeyexchange中使用
secureRenegotiation []byte //安全重协商的时候带上上一次发送的finish消息。 第一次client hello必须为空
secureRenegotiationSupported bool //是否支持安全重协商, 默认支持
alpnProtocols []string //alpn是客户端发送列表, 这样http的第一个请求就知道知否能用pipeline,而不用等待第一个请求返回http的版本号
}
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
func (c *Conn) clientHandshake() error {
hello := &clientHelloMsg{
vers: c.config.maxVersion(),
compressionMethods: []uint8{compressionNone},
random: make([]byte, 32),
serverName: hostnameInSNI(c.config.ServerName),
supportedCurves: c.config.curvePreferences(),
supportedPoints: []uint8{pointFormatUncompressed},
}
hello.cipherSuites = cipherSuites //配置算法套件
_, err := io.ReadFull(c.config.rand(), hello.random) //生成随机数
if sessionCache != nil && c.handshakes == 0 { //session重用不能用在重协商中
// Try to resume a previously negotiated TLS session, if
// available.
cacheKey = clientSessionCacheKey(c.conn.RemoteAddr(), c.config)
candidateSession, ok := sessionCache.Get(cacheKey) //使用severname或者服务端ip作为key
if ok {
...
if versOk && cipherSuiteOk {
session = candidateSession //版本和算法都ok
}
}
if session != nil {
hello.sessionTicket = session.sessionTicket //设置ticket
hello.sessionId = make([]byte, 16)
_, err := io.ReadFull(c.config.rand(), hello.sessionId) //随机生成session id
}
//client hello 写入记录层
if _, err := c.writeRecord(recordTypeHandshake, hello.marshal()); err != nil {
return err
}
...
}

session重用

新建一个tls的连接的时候,如果支持session重用和ticket,先根据服务端ip或者servername作为key,到cache中查找session,如果找到则发送该session的ticket。
ticket是前一次握手完成后保存的协商出来的结果。 在client hello中把ticket发送给服务端,服务端通过ticket密钥解密这个ticket,就能还原上一次协商出来的结果。
可见通过ticket,服务端不用支持session cache。 带上ticket的同时,随机生成一个session id, 如果服务端回显这个session id,则表示支持重用

原来没有ticket的时候,服务端有个session缓存,根据session id来查找,这回导致多机的时候共享session的问题。

NPN和ALPN

客户端通过ALPN扩展,发送应用层协议信息给服务端, 通过服务端的回显来判断上层协议,以便优化上层协议。
比如http 1.1的pipeline就不用通过第一个http response返回才知道知否能够pipeline, 或者上层SPDY/http 2.0具体的版本。
要支持http2.0必须开启NPN或者ALPN

而NPN则是服务端返回上层协议。

scts(signed certificate timestamp support)

scts主要用于certificate transparency。
主要是为了避免某些ca滥发证书, 通过建立第三方审计服务, 让ca/域名拥有者/用户能够检查到该域名是否有被恶意签发。

http://www.certificate-transparency.org/what-is-ct
https://imququ.com/post/certificate-transparency.html

Renegotiation重协商

重协商主要是在握手完成后,在正常通信的过程中,如果希望重新协商密钥信息,就进行重协商
重协商安全问题是因为上层应用没有区分两次不同加密环境的数据。
当攻击者先完成握手,然后再发送用户的client hello来进行重协商, 但是并没有通知上层应用两者的区别,导致攻击者注入非法的报文。

但是安全重协商可以避免这方面的漏洞,通过在client hello中发送secureRenegotiationSupported来标记支持安全重协商。

什么时候需要重协商? 当记录层的序号回绕的时候,但是因为序号64个字节,2^64个记录层报文,就算长连接也很难回绕,golang tls直接panic断开连接。

通常不需要重协商。

压缩算法

在tls使用压缩会造成安全漏洞, 如CRIME攻击,BREACH攻击
https://en.wikipedia.org/wiki/CRIME

ocsp stapling

ocsp是在线证书验证协议,证书中通常包含ocsp验证地址,客户端去该地址验证服务端证书的有效性,如果客户端都去ca的ocsp服务器验证有可能造成响应慢的问题。
因此服务端定期执行ocsp请求,缓存结果,如果客户端支持ocsp stapling就发送缓存的结果。 该结果被ca签名,服务端不能伪造。

SNI

浏览器/客户端支持SNI的话,会把访问的域名作为扩展项发送。
使用SNI发送servername, 主要用于cdn场景,如果client hello不包含servername, cdn就不知道访问的是哪个站点, 不能找到该站点的证书和配置。因为http hostname只能在握手完成后。

但是低版本浏览器不支持该扩展,比如IE8.

ServerHello,Certificate,CertificateStatus,ServerKeyExchange,CertificateRequest,ServerHelloDone

服务端收到client hello后,根据配置协商TLS的版本, 以及算法套件。
协商的算法套件,除了满足两边配置的交集之外, 还跟站点证书的公钥类型有关。 公钥类型决定了可以使用哪种密钥交换什么以及签名算法。

  • 协商后把ServerHello写入记录层准备发出
  • 然后把服务端证书作为Certificate消息
  • 如果支持启用ocsp stapling则把缓存的ocsp response作为CertificateStatus消息
  • 根据密钥协商算法,来发送ServerKeyExchange消息
  • 如果需要客户端证书,发送CertificateRequest请求证书
  • 最后发送ServerHelloDone
  • 把以上握手信息写入记录层后,最后flush记录层,写入tcp层发出
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
type serverHelloMsg struct {
raw []byte
vers uint16 //协商后的版本号
random []byte //服务端随机数
sessionId []byte //回显sessionId,表示client的session重用成功
cipherSuite uint16 //协商后的算法族
compressionMethod uint8 //压缩算法,必须为空算法
nextProtoNeg bool //服务端发送npn消息,表示nextProtos非空
nextProtos []string //返回npn应用协议列表
ocspStapling bool //服务端有ocsp stapling缓存
scts [][]byte //证书列表的sct缓存
ticketSupported bool //服务端会发送新的ticket握手信息
secureRenegotiation []byte //第一次握手必然为空。 安全重协商
secureRenegotiationSupported bool //服务端支持安全重协商。这里客户端支持的话,服务端也支持。
alpnProtocol string //从客户端发送alpn应用协议列表中,如果服务端配置了该协议则回显支持该协议
}
type certificateMsg struct {
raw []byte
certificates [][]byte //服务端证书链
}
type certificateStatusMsg struct {
raw []byte
statusType uint8 //statusTypeOCSP
response []byte //缓存的ocsp response
}
type serverKeyExchangeMsg struct {
raw []byte
key []byte //发送ecdhe的公钥,以及对ecdhe参数和两个hello中random的签名
}
type certificateRequestMsg struct {
raw []byte
// hasSignatureAndHash indicates whether this message includes a list
// of signature and hash functions. This change was introduced with TLS
// 1.2.
hasSignatureAndHash bool
certificateTypes []byte //要求的证书类型,一般为支持rsa或者ecdsa的公钥
signatureAndHashes []signatureAndHash //跟client hello一样,发送支持的签名算法, 用于客户端选择证书
certificateAuthorities [][]byte //发送配置的clientCA subject
}
type serverHelloDoneMsg struct{} //server hello done只是一个标记

服务端serverHandshake()包含了完整的握手流程, 首先调用readClientHello协商信息,在doFullHandshake中处理握手,直到完成premaster secret和master secret的生成。

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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
func (hs *serverHandshakeState) readClientHello() (isResume bool, err error) {
msg, err := c.readHandshake()
var ok bool
hs.clientHello, ok = msg.(*clientHelloMsg) //第一个包必须是client hello消息
if !ok {
c.sendAlert(alertUnexpectedMessage)
return false, unexpectedMessageError(hs.clientHello, msg)
}
c.vers, ok = config.mutualVersion(hs.clientHello.vers) //协商版本
hs.hello = new(serverHelloMsg)
supportedCurve := false
preferredCurves := config.curvePreferences()
Curves:
for _, curve := range hs.clientHello.supportedCurves {
for _, supported := range preferredCurves {
if supported == curve {
supportedCurve = true //有曲线支持
break Curves
}
}
}
supportedPointFormat := false
for _, pointFormat := range hs.clientHello.supportedPoints {
if pointFormat == pointFormatUncompressed {
supportedPointFormat = true //只支持必须有的uncompressed格式
break
}
}
hs.ellipticOk = supportedCurve && supportedPointFormat //曲线和点格式都支持,则可以使用ecc
foundCompression := false
// We only support null compression, so check that the client offered it.
for _, compression := range hs.clientHello.compressionMethods {
if compression == compressionNone {
foundCompression = true
break
}
}
if !foundCompression { //golang tls不支持压缩,client不提供NULL算法的话则不握手
c.sendAlert(alertHandshakeFailure)
return false, errors.New("tls: client does not support uncompressed connections")
}
hs.hello.vers = c.vers //协商后版本
hs.hello.random = make([]byte, 32) //随机数
_, err = io.ReadFull(config.rand(), hs.hello.random)
if len(hs.clientHello.secureRenegotiation) != 0 { //第一次握手不能带重协商信息
c.sendAlert(alertHandshakeFailure)
return false, errors.New("tls: initial handshake had non-empty renegotiation extension")
}
hs.cert, err = config.getCertificate(&ClientHelloInfo{ //如果配置了多个证书选择合适的证书返回,比如根据sni servername信息找到对应的站点证书
CipherSuites: hs.clientHello.cipherSuites,
ServerName: hs.clientHello.serverName,
SupportedCurves: hs.clientHello.supportedCurves,
SupportedPoints: hs.clientHello.supportedPoints,
})
if hs.checkForResumption() { //服务端如果开启ticket,通过ticket还原session,并进行相应的检查
return true, nil
}
//不需要session重用, 开始握手协商
...
for _, id := range preferenceList {
if hs.setCipherSuite(id, supportedList, c.vers) { //协商算法
break
}
}
return false, nil
}
func (c *Conn) serverHandshake() error {
hs := serverHandshakeState{
c: c,
}
isResume, err := hs.readClientHello()
c.buffering = true
if isResume {
//session重用处理
} else {
// The client didn't include a session ticket, or it wasn't
// valid so we do a full handshake.
if err := hs.doFullHandshake(); err != nil { //premaster -> master
return err
}
if err := hs.establishKeys(); err != nil { //从master secret中导出密钥
return err
}
if err := hs.readFinished(c.clientFinished[:]); err != nil { //读取finish消息,及验证两边的历史消息是否一致
return err
}
c.clientFinishedIsFirst = true
c.buffering = true
if err := hs.sendSessionTicket(); err != nil { //收到客户端finish后,发送新的ticket
return err
}
if err := hs.sendFinished(nil); err != nil { //发送服务端finish,服务端完成握手
return err
}
if _, err := c.flush(); err != nil {
return err
}
}
c.handshakeComplete = true
return nil
}
func (hs *serverHandshakeState) doFullHandshake() error {
hs.hello.ticketSupported = hs.clientHello.ticketSupported && !config.SessionTicketsDisabled //两边都支持ticket
hs.hello.cipherSuite = hs.suite.id //协商后的算法
hs.finishedHash = newFinishedHash(hs.c.vers, hs.suite)
hs.finishedHash.Write(hs.clientHello.marshal())
hs.finishedHash.Write(hs.hello.marshal())
if _, err := c.writeRecord(recordTypeHandshake, hs.hello.marshal()); err != nil { //发送server hello
return err
certMsg := new(certificateMsg)
certMsg.certificates = hs.cert.Certificate
hs.finishedHash.Write(certMsg.marshal()) //发送服务端证书
if _, err := c.writeRecord(recordTypeHandshake, certMsg.marshal()); err != nil { //Certificate消息写入记录层
return err
}
if hs.hello.ocspStapling { //使用ocsp stapling, 服务端定期使用ocsp获取证书状态, ocsp被ca签名,服务端不能伪造。 使用ocsp stapling可以让客户端避免去ca服务器检查证书状态,提高tls效率
certStatus := new(certificateStatusMsg)
certStatus.statusType = statusTypeOCSP
certStatus.response = hs.cert.OCSPStaple
hs.finishedHash.Write(certStatus.marshal())
if _, err := c.writeRecord(recordTypeHandshake, certStatus.marshal()); err != nil { //发送CertificateStatus握手消息
return err
}
}
keyAgreement := hs.suite.ka(c.vers) //根据ciphersuite获取密钥交换算法
skx, err := keyAgreement.generateServerKeyExchange(config, hs.cert, hs.clientHello, hs.hello) //如果密钥交换算法要使用serverkey exchange消息则生成,rsa密钥交换不使用该消息
if err != nil {
c.sendAlert(alertHandshakeFailure)
return err
}
if skx != nil {
hs.finishedHash.Write(skx.marshal())
if _, err := c.writeRecord(recordTypeHandshake, skx.marshal()); err != nil { //发送server key exchange
return err
}
}
if config.ClientAuth >= RequestClientCert { //需要客户端证书, 发送CertificateRequest消息
// Request a client certificate
certReq := new(certificateRequestMsg)
certReq.certificateTypes = []byte{
byte(certTypeRSASign),
byte(certTypeECDSASign),
}
if c.vers >= VersionTLS12 {
certReq.hasSignatureAndHash = true
certReq.signatureAndHashes = supportedSignatureAlgorithms
}
if config.ClientCAs != nil {
certReq.certificateAuthorities = config.ClientCAs.Subjects()
}
hs.finishedHash.Write(certReq.marshal()) //发送CertificateRequest消息
if _, err := c.writeRecord(recordTypeHandshake, certReq.marshal()); err != nil {
return err
}
}
helloDone := new(serverHelloDoneMsg) //发送server hello done
hs.finishedHash.Write(helloDone.marshal())
if _, err := c.writeRecord(recordTypeHandshake, helloDone.marshal()); err != nil {
return err
}
if _, err := c.flush(); err != nil {
return err
}
...
}

ServerHello算法协商

算法协商除了了两边的配置列表, 列表中的优先级顺序,还跟站点证书的公钥类型有关。
如果是ECDHE密钥协商,公钥必须支持算法套件的签名算法,比如ECDHE-RSA或者ECDHE-ECDSA, 公钥必须分别支持RSA和ECDSA签名。
如果是RSA密钥协商,公钥必须是RSA公钥。

前向安全(perfect forward secrecy): 即使以后私钥泄露, 也不会导致session key的泄漏。
支持前行安全的密钥协商算法有DHE和ECDHE。
要支持PFS的话,rsa只能用做签名不能用来做交换。 TLS 1.3已经禁用RSA作为密钥交换算法。

1
2
3
4
openssl ciphers -v
ECDHE-RSA-AES256-GCM-SHA384 TLSv1.2 Kx=ECDH Au=RSA Enc=AESGCM(256) Mac=AEAD
ECDHE-ECDSA-AES256-GCM-SHA384 TLSv1.2 Kx=ECDH Au=ECDSA Enc=AESGCM(256) Mac=AEAD
AES256-SHA256 TLSv1.2 Kx=RSA Au=RSA Enc=AES(256) Mac=SHA256

这里Kx是密钥协商算法,Au是签名算法用来认证, Enc是记录层加密算法,Mac是记录层的摘要算法,做完整性验证。
由于GCM作为一种AEAD模式,所以不需要Mac算法。

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
func (hs *serverHandshakeState) setCipherSuite(id uint16, supportedCipherSuites []uint16, version uint16) bool {
for _, supported := range supportedCipherSuites { //遍历client hello中的算法
if id == supported { //服务端支持该算法
var candidate *cipherSuite
for _, s := range cipherSuites {
if s.id == id {
candidate = s //找到该算法的算法操作集合
break
}
}
if candidate == nil {
continue
}
// Don't select a ciphersuite which we can't
// support for this client.
if candidate.flags&suiteECDHE != 0 { //使用ECDHE密钥协商
if !hs.ellipticOk { //client的curve和point format服务端不支持,则返回
continue
}
if candidate.flags&suiteECDSA != 0 { //公钥需要支持ecdsa签名,比如TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384算法
if !hs.ecdsaOk {
continue
}
} else if !hs.rsaSignOk { //必须是rsa公钥,比如TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
continue
}
} else if !hs.rsaDecryptOk { //使用rsa密钥协商,必须是rsa公钥, 比如TLS_RSA_WITH_AES_128_GCM_SHA256
continue
}
if version < VersionTLS12 && candidate.flags&suiteTLS12 != 0 { //tls 1.2only的算法,版本不符合
continue
}
hs.suite = candidate //算法套件符合要求,返回
return true
}
}
return false
}

ServerKeyExchange

ecdhe-rsa或者ecdhe-ecdsa都会调用下面函数生成serverKeyExchange消息

  • 主要根据协商出来的ecc曲线,找到该曲线对应的操作集合
  • 通过随机数生成一个私钥
  • 通过曲线的乘法操作, 把该私钥映射到曲线上的点(x,y), (x,y)作为公钥
  • ecdhParam包含公钥(x,y), 以及曲线curve_id
  • 根据协议对应的摘要算法, 计算digest(client.Random, server.Random, ecdhParam)
  • 使用协商出来的rsa或者ecdsa签名算法,使用证书私钥对该摘要进行签名。
  • ServerKeyExchange中包含ecdhParam和该签名,发送给对方

客户端收到后通过签名,得到可信的ecdhParam
客户端随机生成自己的私钥mpriv,以及对应的公钥(mx, my)
通过服务端公钥(x,y)和mpriv在曲线上乘法计算新的x作为premaster secret
客户端把自己的公钥(mx,my)发送给服务端,服务端计算(mx,my)和自己私钥的乘法得出premaster secret。

总的来说,就是服务端客户端各种生成一个随机数,把这个随机数映射到曲线上的点(x,y)作为公钥, 发送给对方,但是很难逆向映射。
客户端和服务端各自通过对方的公钥和自己的私钥做乘法,得出premaster secret。

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
func (ka *ecdheKeyAgreement) generateServerKeyExchange(config *Config, cert *Certificate, clientHello *clientHelloMsg, hello *serverHelloMsg) (*serverKeyExchangeMsg, error) {
var curveid CurveID
preferredCurves := config.curvePreferences()
NextCandidate:
for _, candidate := range preferredCurves {
for _, c := range clientHello.supportedCurves {
if candidate == c {
curveid = c
break NextCandidate
}
}
}
if curveid == 0 {
return nil, errors.New("tls: no supported elliptic curves offered")
}
//根据client hello和服务端配置的ecc曲线来找到都支持的曲线
var ok bool
if ka.curve, ok = curveForCurveID(curveid); !ok { //返回该曲线上的操作集
return nil, errors.New("tls: preferredCurves includes unsupported curve")
}
var x, y *big.Int
var err error
ka.privateKey, x, y, err = elliptic.GenerateKey(ka.curve, config.rand()) //随机生成private key, 并返回坐标x,y
if err != nil {
return nil, err
}
ecdhePublic := elliptic.Marshal(ka.curve, x, y) //x,y作为公钥
// http://tools.ietf.org/html/rfc4492#section-5.4
serverECDHParams := make([]byte, 1+2+1+len(ecdhePublic))
serverECDHParams[0] = 3 // named curve
serverECDHParams[1] = byte(curveid >> 8)
serverECDHParams[2] = byte(curveid)
serverECDHParams[3] = byte(len(ecdhePublic))
copy(serverECDHParams[4:], ecdhePublic)
sigAndHash := signatureAndHash{signature: ka.sigType}
if ka.version >= VersionTLS12 { //tls 1.2必须使用client hello发送的签名算法
if sigAndHash.hash, err = pickTLS12HashForSignature(ka.sigType, clientHello.signatureAndHashes); err != nil {
return nil, err
}
}
//对两个随机数,和ecc参数一起做摘要,具体的摘要算法,如果是tls1.2则由client hello协商,否则使用固定的摘要算法
digest, hashFunc, err := hashForServerKeyExchange(sigAndHash, ka.version, clientHello.random, hello.random, serverECDHParams)
if err != nil {
return nil, err
}
priv, ok := cert.PrivateKey.(crypto.Signer)
if !ok {
return nil, errors.New("tls: certificate private key does not implement crypto.Signer")
}
var sig []byte
switch ka.sigType {
case signatureECDSA:
_, ok := priv.Public().(*ecdsa.PublicKey)
if !ok {
return nil, errors.New("tls: ECDHE ECDSA requires an ECDSA server key")
}
case signatureRSA:
_, ok := priv.Public().(*rsa.PublicKey)
if !ok {
return nil, errors.New("tls: ECDHE RSA requires a RSA server key")
}
default:
return nil, errors.New("tls: unknown ECDHE signature algorithm")
}
sig, err = priv.Sign(config.rand(), digest, hashFunc) //根据公钥类型,用对应的ecdsa或者rsa签名算法,使用私钥对摘要进行签名
if err != nil {
return nil, errors.New("tls: failed to sign ECDHE parameters: " + err.Error())
}
skx := new(serverKeyExchangeMsg)
sigAndHashLen := 0
if ka.version >= VersionTLS12 {
sigAndHashLen = 2
}
skx.key = make([]byte, len(serverECDHParams)+sigAndHashLen+2+len(sig))
copy(skx.key, serverECDHParams) //包含ecdh参数
k := skx.key[len(serverECDHParams):]
if ka.version >= VersionTLS12 {
k[0] = sigAndHash.hash
k[1] = sigAndHash.signature
k = k[2:]
}
k[0] = byte(len(sig) >> 8) //sign(digest(client.Random, server.Random, ecdhParam))
k[1] = byte(len(sig))
copy(k[2:], sig)
return skx, nil
}

Certificate,ClientKeyExchange,CertificateVerify,ChangeCipherSpec,nextProto,finished

客户端收到serverHello到serverHelloDone之间的消息后

  • serverHello中的信息完成随机数的交换,算法套件,版本号等的协商
  • 如果收到CertificateRequest消息,则选择合适的客户端证书,发送Certificate消息
  • 如果收到CertificateStatus消息,则保存ocsp验证结果
  • 如果收到ServerKeyExchange消息,说明不是rsa密钥协商,很可能是ecdhe密钥协商,客户端通过该验证该信息的签名,然后再从中计算premaster secret,并发送client key exchange,让服务端据此计算premaster secret.
  • 如果客户端要发送证书,再发送一条CertificateVerify, 使用私钥对历史握手信息签名,以表示自己拥有该证书的私钥
  • 根据premaster secret通过PRF导出master secret,再从master secret中导出clientMAC, serverMAC, clientKey, serverKey, clientIV, serverIV
  • 从此完成密钥协商算法,发送ChangeCipherSpec消息,表示使用新的密钥环境。
  • 如果是serverHello中发送了NPN应用层协议, 客户端发送nextProto消息,表明应用层协议协商的结果
  • 最后通过master_secret和历史握手信息摘要,用prf导出finish消息发送给服务端,让服务端验证。 通常verify_data_length=12
    PRF(master_secret, “client finished”, Hash(handshake_messages)) [0..verify_data_length-1];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type clientKeyExchangeMsg struct {
raw []byte
ciphertext []byte
}
type certificateVerifyMsg struct {
raw []byte
hasSignatureAndHash bool
signatureAndHash signatureAndHash //从certificateRequest消息的算法中选择一种
signature []byte //客户端私钥对历史数据的摘要签名
}
type nextProtoMsg struct {
raw []byte
proto string //使用NPN时,返回应用程序的上层协议
}
type finishedMsg struct {
raw []byte
verifyData []byte
}

ClientKeyExchange

ECDHE协商

对于ECDHE算法的描述,见上文ServerKeyExchange, 收到serverkeyexchange就收到了服务端的ecdhe公钥,同时通过服务端证书的公钥验证签名。
然后客户端生成premaster secret, 再把客户端的ecdhe公钥通过clientKeyExchange发送给服务端

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
func (ka *ecdheKeyAgreement) processServerKeyExchange(config *Config, clientHello *clientHelloMsg, serverHello *serverHelloMsg, cert *x509.Certificate, skx *serverKeyExchangeMsg) error {
if len(skx.key) < 4 {
return errServerKeyExchange
}
if skx.key[0] != 3 { // named curve
return errors.New("tls: server selected unsupported curve")
}
curveid := CurveID(skx.key[1])<<8 | CurveID(skx.key[2]) //读取ecdhParam中的curve_id
var ok bool
if ka.curve, ok = curveForCurveID(curveid); !ok {
return errors.New("tls: server selected unsupported curve")
}
publicLen := int(skx.key[3])
if publicLen+4 > len(skx.key) {
return errServerKeyExchange
}
ka.x, ka.y = elliptic.Unmarshal(ka.curve, skx.key[4:4+publicLen]) ////读取ecdhParam中的公钥(x,y)
if ka.x == nil {
return errServerKeyExchange
}
if !ka.curve.IsOnCurve(ka.x, ka.y) { //(x,y)必然在曲线上
return errServerKeyExchange
}
serverECDHParams := skx.key[:4+publicLen]
sig := skx.key[4+publicLen:]
if len(sig) < 2 {
return errServerKeyExchange
}
sigAndHash := signatureAndHash{signature: ka.sigType} //ecdhe-rsa使用rsa签名,ecdhe-ecdsa使用ecdsa签名
if ka.version >= VersionTLS12 { //tls 1.2使用协商的签名摘要算法
// handle SignatureAndHashAlgorithm
sigAndHash = signatureAndHash{hash: sig[0], signature: sig[1]}
if sigAndHash.signature != ka.sigType {
return errServerKeyExchange
}
sig = sig[2:]
if len(sig) < 2 {
return errServerKeyExchange
}
}
sigLen := int(sig[0])<<8 | int(sig[1])
if sigLen+2 != len(sig) {
return errServerKeyExchange
}
sig = sig[2:]
//digest(client.Random, server.Random, ecdhParam)
digest, hashFunc, err := hashForServerKeyExchange(sigAndHash, ka.version, clientHello.random, serverHello.random, serverECDHParams)
if err != nil {
return err
}
//开始验证签名
switch ka.sigType {
case signatureECDSA:
pubKey, ok := cert.PublicKey.(*ecdsa.PublicKey)
if !ok {
return errors.New("tls: ECDHE ECDSA requires a ECDSA server public key")
}
ecdsaSig := new(ecdsaSignature)
if _, err := asn1.Unmarshal(sig, ecdsaSig); err != nil {
return err
}
if ecdsaSig.R.Sign() <= 0 || ecdsaSig.S.Sign() <= 0 {
return errors.New("tls: ECDSA signature contained zero or negative values")
}
if !ecdsa.Verify(pubKey, digest, ecdsaSig.R, ecdsaSig.S) {
return errors.New("tls: ECDSA verification failure")
}
case signatureRSA:
pubKey, ok := cert.PublicKey.(*rsa.PublicKey)
if !ok {
return errors.New("tls: ECDHE RSA requires a RSA server public key")
}
if err := rsa.VerifyPKCS1v15(pubKey, hashFunc, digest, sig); err != nil {
return err
}
default:
return errors.New("tls: unknown ECDHE signature algorithm")
}
return nil
}
func (ka *ecdheKeyAgreement) generateClientKeyExchange(config *Config, clientHello *clientHelloMsg, cert *x509.Certificate) ([]byte, *clientKeyExchangeMsg, error) {
if ka.curve == nil {
return nil, nil, errors.New("tls: missing ServerKeyExchange message")
}
priv, mx, my, err := elliptic.GenerateKey(ka.curve, config.rand()) //客户端生成私钥,并转换到对应的公钥(mx,my)
if err != nil {
return nil, nil, err
}
x, _ := ka.curve.ScalarMult(ka.x, ka.y, priv) //通过收到的服务端公钥(x,y)和客户端私钥, 通过曲线乘法来计算(x,_)
preMasterSecret := make([]byte, (ka.curve.Params().BitSize+7)>>3)
xBytes := x.Bytes()
copy(preMasterSecret[len(preMasterSecret)-len(xBytes):], xBytes)
serialized := elliptic.Marshal(ka.curve, mx, my)
ckx := new(clientKeyExchangeMsg)
ckx.ciphertext = make([]byte, 1+len(serialized))
ckx.ciphertext[0] = byte(len(serialized))
copy(ckx.ciphertext[1:], serialized) //把公钥(mx,my)发送给服务端
return preMasterSecret, ckx, nil
}

RSA密钥协商

RSA协商的话没有serverKeyExchange消息,同时RSA密钥协商由于没有前向安全性,所以被tls1.3禁用了。

RSA密钥协商较为简单,主要是在clientKeyExchange中随机生成premaster secret, 然后使用服务端证书的公钥加密。
服务端收到serverKeyExchange消息后,使用私钥解密,就能得到premaster secret。
同时加密的时候引入随机数,解密的时候忽略这块随机数,为了让每次加密同一个premaster的时候得到不同的密文。
另外还有一种rsa blinding攻击,通过解密的时间猜测密钥的范围。
在解密的时候引入随机变量,增加一些计算可以避免blinding攻击。

http://nxlhero.blog.51cto.com/962631/1832127

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
func (ka rsaKeyAgreement) generateClientKeyExchange(config *Config, clientHello *clientHelloMsg, cert *x509.Certificate) ([]byte, *clientKeyExchangeMsg, error) {
preMasterSecret := make([]byte, 48)
preMasterSecret[0] = byte(clientHello.vers >> 8) //client hello的版本
preMasterSecret[1] = byte(clientHello.vers)
_, err := io.ReadFull(config.rand(), preMasterSecret[2:]) //随机生成premaster secret
if err != nil {
return nil, nil, err
}
encrypted, err := rsa.EncryptPKCS1v15(config.rand(), cert.PublicKey.(*rsa.PublicKey), preMasterSecret) //使用公钥加密premaster secret, 引入随机参数是为了每次加密相同的数据也不会有相同的结果
if err != nil {
return nil, nil, err
}
ckx := new(clientKeyExchangeMsg)
ckx.ciphertext = make([]byte, len(encrypted)+2)
ckx.ciphertext[0] = byte(len(encrypted) >> 8)
ckx.ciphertext[1] = byte(len(encrypted))
copy(ckx.ciphertext[2:], encrypted)
return preMasterSecret, ckx, nil
}
func (ka rsaKeyAgreement) processClientKeyExchange(config *Config, cert *Certificate, ckx *clientKeyExchangeMsg, version uint16) ([]byte, error) {
if len(ckx.ciphertext) < 2 {
return nil, errClientKeyExchange
}
ciphertext := ckx.ciphertext
if version != VersionSSL30 {
ciphertextLen := int(ckx.ciphertext[0])<<8 | int(ckx.ciphertext[1])
if ciphertextLen != len(ckx.ciphertext)-2 {
return nil, errClientKeyExchange
}
ciphertext = ckx.ciphertext[2:]
}
priv, ok := cert.PrivateKey.(crypto.Decrypter)
if !ok {
return nil, errors.New("tls: certificate private key does not implement crypto.Decrypter")
}
// Perform constant time RSA PKCS#1 v1.5 decryption
preMasterSecret, err := priv.Decrypt(config.rand(), ciphertext, &rsa.PKCS1v15DecryptOptions{SessionKeyLen: 48}) //使用私钥解密client key exchange
if err != nil {
return nil, err
}
// We don't check the version number in the premaster secret. For one,
// by checking it, we would leak information about the validity of the
// encrypted pre-master secret. Secondly, it provides only a small
// benefit against a downgrade attack and some implementations send the
// wrong version anyway. See the discussion at the end of section
// 7.4.7.1 of RFC 4346.
return preMasterSecret, nil
}

NewSessionTicket,ChangeCipherSpec,Finish

服务端发送serverHelloDone, 收到后续的握手包处理

  • 如果发送了证书请求, 就会收到客户端的certificate消息,包含满足cetificateReq消息的客户端证书,如果双向认证,就验证客户端证书
  • 等待clientKeyExchange消息, 这时候client已经计算出了master secret,通过clientKeyExchange就能计算出服务端的premaster secret,进而导出 master secret
  • 如果收到客户端证书,等待certificateVerify消息,该消息保护客户端私钥的签名,表情客户端拥有该证书的私钥
  • 读取客户端的changeCipherSpec消息
  • 如果服务端发送了NPN,则客户端收到后发送nextProtoMsg消息,表明客户端接受该应用程协议。
  • 读取客户端的Finish消息,验证finish消息是否正确。 通过本地的master secret和历史握手信息计算本地的finish消息,跟客户端的finish对比。 得出本次握手协商的正确性以及未被篡改。
  • 发送NewSessionTicket消息,支持session重用
  • 发送ChangeCipherSpec和Finish
  • 自此sever端完成握手,开始从记录层读取应用消息
  • 客户端收到服务端的New Ticket后,保存到cache, 最后验证Finish消息,完成握手。
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
152
func (hs *serverHandshakeState) doFullHandshake() error {
...
//发送server hello done
var ok bool
// If we requested a client certificate, then the client must send a
// certificate message, even if it's empty.
if config.ClientAuth >= RequestClientCert { //如果发送了证书请求,下一个消息应该为客户端证书
if certMsg, ok = msg.(*certificateMsg); !ok {
c.sendAlert(alertUnexpectedMessage)
return unexpectedMessageError(certMsg, msg)
}
hs.finishedHash.Write(certMsg.marshal())
pub, err = hs.processCertsFromClient(certMsg.certificates) //解析及验证client证书,返回公钥
msg, err = c.readHandshake()
}
// Get client key exchange
ckx, ok := msg.(*clientKeyExchangeMsg) //获取下一个client key exchange消息
if !ok {
c.sendAlert(alertUnexpectedMessage)
return unexpectedMessageError(ckx, msg)
}
hs.finishedHash.Write(ckx.marshal())
preMasterSecret, err := keyAgreement.processClientKeyExchange(config, hs.cert, ckx, c.vers) //服务端私钥解密client key exchange消息,获得pre-master secret
if err != nil {
c.sendAlert(alertHandshakeFailure)
return err
}
hs.masterSecret = masterFromPreMasterSecret(c.vers, hs.suite, preMasterSecret, hs.clientHello.random, hs.hello.random) //preMaster secret计算master secret
if len(c.peerCertificates) > 0 { //如果收到客户端证书,这里收到certificateVerify消息, 使用客户端公钥验证签名
...
switch key := pub.(type) { //公钥解密,验证签名
case *ecdsa.PublicKey:
...
if !ecdsa.Verify(key, digest, ecdsaSig.R, ecdsaSig.S) {
err = errors.New("tls: ECDSA verification failure")
}
case *rsa.PublicKey:
...
err = rsa.VerifyPKCS1v15(key, hashFunc, digest, certVerify.signature) //验证签名
}
if err != nil {
c.sendAlert(alertBadCertificate)
return errors.New("tls: could not validate signature of connection nonces: " + err.Error())
}
hs.finishedHash.Write(certVerify.marshal())
}
hs.finishedHash.discardHandshakeBuffer()
return nil
}
func (hs *serverHandshakeState) readFinished(out []byte) error {
c := hs.c
c.readRecord(recordTypeChangeCipherSpec) //读取客户端changeCipherSpec
if c.in.err != nil {
return c.in.err
}
if hs.hello.nextProtoNeg { //如果服务端发了npn扩展
msg, err := c.readHandshake()
if err != nil {
return err
}
nextProto, ok := msg.(*nextProtoMsg) //读取客户端nextproto消息
if !ok {
c.sendAlert(alertUnexpectedMessage)
return unexpectedMessageError(nextProto, msg)
}
hs.finishedHash.Write(nextProto.marshal())
c.clientProtocol = nextProto.proto
}
msg, err := c.readHandshake()
if err != nil {
return err
}
clientFinished, ok := msg.(*finishedMsg) //读取finish
if !ok {
c.sendAlert(alertUnexpectedMessage)
return unexpectedMessageError(clientFinished, msg)
}
verify := hs.finishedHash.clientSum(hs.masterSecret)
if len(verify) != len(clientFinished.verifyData) ||
subtle.ConstantTimeCompare(verify, clientFinished.verifyData) != 1 { //比较client和server端两边的历史消息的摘要以及mastersecret,一致则说明没有被篡改,完成握手
c.sendAlert(alertHandshakeFailure)
return errors.New("tls: client's Finished message is incorrect")
}
hs.finishedHash.Write(clientFinished.marshal())
copy(out, verify)
return nil
}
func (hs *serverHandshakeState) sendFinished(out []byte) error {
c := hs.c
if _, err := c.writeRecord(recordTypeChangeCipherSpec, []byte{1}); err != nil { //发送changeCipherSpec
return err
}
finished := new(finishedMsg)
finished.verifyData = hs.finishedHash.serverSum(hs.masterSecret)
hs.finishedHash.Write(finished.marshal())
if _, err := c.writeRecord(recordTypeHandshake, finished.marshal()); err != nil { //发送服务端finish
return err
}
c.cipherSuite = hs.suite.id
copy(out, finished.verifyData)
return nil
}
type newSessionTicketMsg struct {
raw []byte
ticket []byte
}
func (hs *serverHandshakeState) sendSessionTicket() error {
if !hs.hello.ticketSupported {
return nil
}
c := hs.c
m := new(newSessionTicketMsg)
var err error
state := sessionState{
vers: c.vers, //协商出来的版本
cipherSuite: hs.suite.id, //协商出来的算法
masterSecret: hs.masterSecret, //协商出来的master secret
certificates: hs.certsFromClient, //客户端证书,如果有
}
m.ticket, err = c.encryptTicket(&state)
if err != nil {
return err
}
hs.finishedHash.Write(m.marshal())
if _, err := c.writeRecord(recordTypeHandshake, m.marshal()); err != nil {
return err
}
return nil
}

ticket计算

1
2
3
4
5
6
struct {
opaque key_name[16];
opaque iv[16];
opaque encrypted_state<0..2^16-1>;
opaque mac[20];
} ticket;

在服务启动后,如果支持ticket,则初始化ticket的aesKey和macKey。
读取客户端finish消息并验证之后,通过aeskey对协商出来的session信息加密,然后使用hmac摘要防伪造。

值得一提的是,很明显可以知道ticket安全性完全靠这两个随机生成的key, 同时ticket并没有加密过期时间,
服务端收到ticket判断session重用的时候不会检测超时时间!!

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
type ticketKey struct {
// keyName is an opaque byte string that serves to identify the session
// ticket key. It's exposed as plaintext in every session ticket.
keyName [ticketKeyNameLen]byte
aesKey [16]byte
hmacKey [16]byte
}
func ticketKeyFromBytes(b [32]byte) (key ticketKey) {
hashed := sha512.Sum512(b[:])
copy(key.keyName[:], hashed[:ticketKeyNameLen])
copy(key.aesKey[:], hashed[ticketKeyNameLen:ticketKeyNameLen+16])
copy(key.hmacKey[:], hashed[ticketKeyNameLen+16:ticketKeyNameLen+32])
return key
}
func (c *Conn) encryptTicket(state *sessionState) ([]byte, error) { //计算ticket,发送给client
serialized := state.marshal() //协商出来的版本,算法套件,master secret,客户端证书如果有的话
encrypted := make([]byte, ticketKeyNameLen+aes.BlockSize+len(serialized)+sha256.Size)
keyName := encrypted[:ticketKeyNameLen]
iv := encrypted[ticketKeyNameLen : ticketKeyNameLen+aes.BlockSize]
macBytes := encrypted[len(encrypted)-sha256.Size:] //最后的sha256摘要的hmac
if _, err := io.ReadFull(c.config.rand(), iv); err != nil { //随机生成iv
return nil, err
}
key := c.config.ticketKeys()[0] //只有第一个key被用来创建ticket,服务启动的时候随机生成
copy(keyName, key.keyName[:])
block, err := aes.NewCipher(key.aesKey[:])
if err != nil {
return nil, errors.New("tls: failed to create cipher while encrypting ticket: " + err.Error())
}
cipher.NewCTR(block, iv).XORKeyStream(encrypted[ticketKeyNameLen+aes.BlockSize:], serialized) //使用计数器模式进行异或
mac := hmac.New(sha256.New, key.hmacKey[:]) //hmac计算摘要
mac.Write(encrypted[:len(encrypted)-sha256.Size])
mac.Sum(macBytes[:0])
return encrypted, nil
}
//解密ticket后就能获得上一次的协商结果
func (c *Conn) decryptTicket(encrypted []byte) (*sessionState, bool) {
if c.config.SessionTicketsDisabled ||
len(encrypted) < ticketKeyNameLen+aes.BlockSize+sha256.Size {
return nil, false
}
keyName := encrypted[:ticketKeyNameLen]
iv := encrypted[ticketKeyNameLen : ticketKeyNameLen+aes.BlockSize]
macBytes := encrypted[len(encrypted)-sha256.Size:]
keys := c.config.ticketKeys()
keyIndex := -1
for i, candidateKey := range keys { //所有的ticket key都能被用来解密
if bytes.Equal(keyName, candidateKey.keyName[:]) {
keyIndex = i
break
}
}
if keyIndex == -1 {
return nil, false
}
key := &keys[keyIndex]
mac := hmac.New(sha256.New, key.hmacKey[:])
mac.Write(encrypted[:len(encrypted)-sha256.Size])
expected := mac.Sum(nil)
if subtle.ConstantTimeCompare(macBytes, expected) != 1 { //ticket的mac部分验证
return nil, false
}
block, err := aes.NewCipher(key.aesKey[:])
if err != nil {
return nil, false
}
ciphertext := encrypted[ticketKeyNameLen+aes.BlockSize : len(encrypted)-sha256.Size]
plaintext := ciphertext
cipher.NewCTR(block, iv).XORKeyStream(plaintext, ciphertext)
state := &sessionState{usedOldKey: keyIndex > 0}
ok := state.unmarshal(plaintext)
return state, ok
}

session重用

  • clientHello的时候通过域名或者服务端ip,从session cache中找到ticket,作为clientHello的扩展带给服务端
  • 服务端收到后,先进行协商,然后再解密ticket, 得到session信息,检查session中的版本算法套件,证书信息是否和协商的结果一致。
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
func (hs *serverHandshakeState) checkForResumption() bool {
c := hs.c
if c.config.SessionTicketsDisabled {
return false
}
var ok bool
var sessionTicket = append([]uint8{}, hs.clientHello.sessionTicket...)
if hs.sessionState, ok = c.decryptTicket(sessionTicket); !ok { //解密session ticket
return false
}
//ticket还原session成功,检查版本号及双方是否依然支持该算法
// Never resume a session for a different TLS version.
if c.vers != hs.sessionState.vers {
return false
}
cipherSuiteOk := false
// Check that the client is still offering the ciphersuite in the session.
for _, id := range hs.clientHello.cipherSuites {
if id == hs.sessionState.cipherSuite {
cipherSuiteOk = true
break
}
}
if !cipherSuiteOk {
return false
}
// Check that we also support the ciphersuite from the session.
if !hs.setCipherSuite(hs.sessionState.cipherSuite, c.config.cipherSuites(), hs.sessionState.vers) {
return false
}
sessionHasClientCerts := len(hs.sessionState.certificates) != 0
needClientCerts := c.config.ClientAuth == RequireAnyClientCert || c.config.ClientAuth == RequireAndVerifyClientCert
if needClientCerts && !sessionHasClientCerts { //如果需要验证客户端证书而session中没有
return false
}
if sessionHasClientCerts && c.config.ClientAuth == NoClientCert {
return false
}
return true
}
  • 如果服务端判断可以session重用,则回显client hello的sessionId, 发送serverHello
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
func (hs *serverHandshakeState) doResumeHandshake() error {
c := hs.c
hs.hello.cipherSuite = hs.suite.id
// We echo the client's session ID in the ServerHello to let it know
// that we're doing a resumption.
hs.hello.sessionId = hs.clientHello.sessionId //回显session id表示接受session重用
hs.hello.ticketSupported = hs.sessionState.usedOldKey //解密ticket的不是第一个key,说明服务端ticket key有多个
hs.finishedHash = newFinishedHash(c.vers, hs.suite)
hs.finishedHash.discardHandshakeBuffer()
hs.finishedHash.Write(hs.clientHello.marshal()) //client hello hash
hs.finishedHash.Write(hs.hello.marshal()) //server hello hash
if _, err := c.writeRecord(recordTypeHandshake, hs.hello.marshal()); err != nil {
return err
}
if len(hs.sessionState.certificates) > 0 {
if _, err := hs.processCertsFromClient(hs.sessionState.certificates); err != nil {
return err
}
}
hs.masterSecret = hs.sessionState.masterSecret //使用session中的master secret
return nil
}
  • 从session的master secret导出密钥,因为随机数等不同,所以session key跟之前的是不一样的
  • 如果ticket的密钥更换过了,可以发送新的ticket
  • 最后发送changeCipherSpec和finish消息,然后等待客户端的changeCipherSpec和finish

密钥导出

密钥导出都是用PRF算法导出特定的长度信息

premaster secret->master secret

1
2
3
4
5
6
7
8
9
10
11
// masterFromPreMasterSecret generates the master secret from the pre-master
// secret. See http://tools.ietf.org/html/rfc5246#section-8.1
func masterFromPreMasterSecret(version uint16, suite *cipherSuite, preMasterSecret, clientRandom, serverRandom []byte) []byte {
seed := make([]byte, 0, len(clientRandom)+len(serverRandom))
seed = append(seed, clientRandom...)
seed = append(seed, serverRandom...)
masterSecret := make([]byte, masterSecretLength) //48字节
prfForVersion(version, suite)(masterSecret, preMasterSecret, masterSecretLabel, seed) //导出master secret
return masterSecret
}

master secret导出密钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// keysFromMasterSecret generates the connection keys from the master
// secret, given the lengths of the MAC key, cipher key and IV, as defined in
// RFC 2246, section 6.3.
func keysFromMasterSecret(version uint16, suite *cipherSuite, masterSecret, clientRandom, serverRandom []byte, macLen, keyLen, ivLen int) (clientMAC, serverMAC, clientKey, serverKey, clientIV, serverIV []byte) {
seed := make([]byte, 0, len(serverRandom)+len(clientRandom))
seed = append(seed, serverRandom...)
seed = append(seed, clientRandom...)
n := 2*macLen + 2*keyLen + 2*ivLen
keyMaterial := make([]byte, n)
prfForVersion(version, suite)(keyMaterial, masterSecret, keyExpansionLabel, seed)
clientMAC = keyMaterial[:macLen]
keyMaterial = keyMaterial[macLen:]
serverMAC = keyMaterial[:macLen]
keyMaterial = keyMaterial[macLen:]
clientKey = keyMaterial[:keyLen]
keyMaterial = keyMaterial[keyLen:]
serverKey = keyMaterial[:keyLen]
keyMaterial = keyMaterial[keyLen:]
clientIV = keyMaterial[:ivLen]
keyMaterial = keyMaterial[ivLen:]
serverIV = keyMaterial[:ivLen]
return
}
  • 为什么每个方向都要对应的key?
    避免知道单个方向的明文,而造成的攻击。

比如streamcipher, 如果攻击者知道TLS数据流一个方向的部分明文,那么对2个方向的密文做一下xor,就能得到另一个方向对应部分的明文了。
还有AEAD也规定了不能使用相同的key+nonce来加密不同的明文,故如果TLS双方使用相同的key,又从相同的数字开始给nonce递增,那就不符合规定,会直接导致aes-gcm 被攻破。

  • 各个key什么用?
    cbc算法使用先hmac后加密的方式, hmac中使用mac key来做认证。
    serverKey和clientKey是对称密钥做记录层的对称加密。
    如果使用cbc算法, 在tls1.1之前,使用clientIV, serverIV作为IV。
    AEAD算法先做加密后摘要的方式,更加安全, 不使用mac key。

PRF算法

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
func prfAndHashForVersion(version uint16, suite *cipherSuite) (func(result, secret, label, seed []byte), crypto.Hash) {
switch version {
case VersionSSL30:
return prf30, crypto.Hash(0)
case VersionTLS10, VersionTLS11:
return prf10, crypto.Hash(0)
case VersionTLS12:
if suite.flags&suiteSHA384 != 0 {
return prf12(sha512.New384), crypto.SHA384
}
return prf12(sha256.New), crypto.SHA256
default:
panic("unknown version")
}
}
// prf12 implements the TLS 1.2 pseudo-random function, as defined in RFC 5246, section 5.
func prf12(hashFunc func() hash.Hash) func(result, secret, label, seed []byte) {
return func(result, secret, label, seed []byte) {
labelAndSeed := make([]byte, len(label)+len(seed))
copy(labelAndSeed, label)
copy(labelAndSeed[len(label):], seed)
pHash(result, secret, labelAndSeed, hashFunc)
}
}
// pHash implements the P_hash function, as defined in RFC 4346, section 5.
/*
P_hash(secret, seed) = HMAC_hash(secret, A(1) + seed) +
HMAC_hash(secret, A(2) + seed) +
HMAC_hash(secret, A(3) + seed) + ...
A() is defined as:
A(0) = seed
A(i) = HMAC_hash(secret, A(i-1))
*/
func pHash(result, secret, seed []byte, hash func() hash.Hash) {
h := hmac.New(hash, secret) //premaster secret作为密钥,
h.Write(seed) //对seed做hmac
a := h.Sum(nil) //计算A(1)
j := 0
for j < len(result) { //hmac直到满足master secret的长度
h.Reset()
h.Write(a)
h.Write(seed)
b := h.Sum(nil) //HMAC_hash(secret, A(i-1) + seed)
todo := len(b)
if j+todo > len(result) {
todo = len(result) - j
}
copy(result[j:j+todo], b)
j += todo
h.Reset()
h.Write(a)
a = h.Sum(nil)
}
}

Hello Request

在完成握手后,如果服务端希望重协商,让客户端重新发送client hello,服务端就好发送Hello Request消息。

记录层

TLS的压缩不安全, golang tls记录层也不实现压缩。另外每个记录层包的最大明文数据为16k
golang tls的实现明显可以看到数据是先copy到记录层,有一层拷贝的存在,没有在数据上做本地操作。

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
const maxPlaintext = 16384 // maximum plaintext payload length
type recordType uint8
const (
recordTypeChangeCipherSpec recordType = 20
recordTypeAlert recordType = 21
recordTypeHandshake recordType = 22
recordTypeApplicationData recordType = 23
)
// TLS handshake message types.
const (
typeHelloRequest uint8 = 0
typeClientHello uint8 = 1
typeServerHello uint8 = 2
typeNewSessionTicket uint8 = 4
typeCertificate uint8 = 11
typeServerKeyExchange uint8 = 12
typeCertificateRequest uint8 = 13
typeServerHelloDone uint8 = 14
typeCertificateVerify uint8 = 15
typeClientKeyExchange uint8 = 16
typeFinished uint8 = 20
typeCertificateStatus uint8 = 22
typeNextProtocol uint8 = 67 // Not IANA assigned
)

加密

握手数据因为还没有协商出算法,因此不做加密的。完成握手后

  • 如果是cbc模式, 对于tls1.1以上版本,每个记录层包都要随机生成IV,防止BEAST攻击
  • 如果是AEAD模式, golang使用记录层序号作为AEAD的nonce
  • cbc模式先做hmac,后做对称加密,然后发出
  • AEAD模式先做加密,后做摘要
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
func (c *Conn) writeRecordLocked(typ recordType, data []byte) (int, error) {
b := c.out.newBlock()
defer c.out.freeBlock(b)
var n int
for len(data) > 0 {
explicitIVLen := 0
explicitIVIsSeq := false
var cbc cbcMode
if c.out.version >= VersionTLS11 {
var ok bool
if cbc, ok = c.out.cipher.(cbcMode); ok {
explicitIVLen = cbc.BlockSize() //tls1.1 cbc模式需要explicit IV
}
}
if explicitIVLen == 0 {
if _, ok := c.out.cipher.(cipher.AEAD); ok {
explicitIVLen = 8
// The AES-GCM construction in TLS has an
// explicit nonce so that the nonce can be
// random. However, the nonce is only 8 bytes
// which is too small for a secure, random
// nonce. Therefore we use the sequence number
// as the nonce.
explicitIVIsSeq = true //使用序号作为AEAD的nonce
}
}
m := len(data)
if maxPayload := c.maxPayloadSizeForWrite(typ, explicitIVLen); m > maxPayload {
m = maxPayload
}
b.resize(recordHeaderLen + explicitIVLen + m)
b.data[0] = byte(typ)
vers := c.vers
if vers == 0 {
// Some TLS servers fail if the record version is
// greater than TLS 1.0 for the initial ClientHello.
vers = VersionTLS10
}
b.data[1] = byte(vers >> 8)
b.data[2] = byte(vers) //版本
b.data[3] = byte(m >> 8) //长度
b.data[4] = byte(m)
if explicitIVLen > 0 {
explicitIV := b.data[recordHeaderLen : recordHeaderLen+explicitIVLen]
if explicitIVIsSeq {
copy(explicitIV, c.out.seq[:]) //AEAD算法使用序号作为nonce
} else {
if _, err := io.ReadFull(c.config.rand(), explicitIV); err != nil { //随机生成cbc算法的explicit IV
return n, err
}
}
}
copy(b.data[recordHeaderLen+explicitIVLen:], data) //拷贝数据到记录层缓存
c.out.encrypt(b, explicitIVLen)
if _, err := c.write(b.data); err != nil {
return n, err
}
n += m
data = data[m:]
}
if typ == recordTypeChangeCipherSpec {
if err := c.out.changeCipherSpec(); err != nil {
return n, c.sendAlertLocked(err.(alert))
}
}
return n, nil
}
func (hc *halfConn) encrypt(b *block, explicitIVLen int) (bool, alert) {
// mac
if hc.mac != nil { //mac包含序号,防止重放和伪造, AEAD不包含mac算法
mac := hc.mac.MAC(hc.outDigestBuf, hc.seq[0:], b.data[:recordHeaderLen], b.data[recordHeaderLen+explicitIVLen:])
n := len(b.data)
b.resize(n + len(mac))
copy(b.data[n:], mac)
hc.outDigestBuf = mac
} //cbc模式先做mac,后加密
payload := b.data[recordHeaderLen:]
// encrypt
if hc.cipher != nil {
switch c := hc.cipher.(type) {
case cipher.Stream:
c.XORKeyStream(payload, payload)
case cipher.AEAD: //AEAD模式先加密后mac,更安全, 同时更少的cpu使用率
payloadLen := len(b.data) - recordHeaderLen - explicitIVLen
b.resize(len(b.data) + c.Overhead())
nonce := b.data[recordHeaderLen : recordHeaderLen+explicitIVLen]
payload := b.data[recordHeaderLen+explicitIVLen:]
payload = payload[:payloadLen]
copy(hc.additionalData[:], hc.seq[:])
copy(hc.additionalData[8:], b.data[:3])
hc.additionalData[11] = byte(payloadLen >> 8)
hc.additionalData[12] = byte(payloadLen)
c.Seal(payload[:0], nonce, payload, hc.additionalData[:])
case cbcMode:
blockSize := c.BlockSize()
if explicitIVLen > 0 {
c.SetIV(payload[:explicitIVLen]) //tls1.1 每个record设置随机iv
payload = payload[explicitIVLen:]
}
prefix, finalBlock := padToBlockSize(payload, blockSize)
b.resize(recordHeaderLen + explicitIVLen + len(prefix) + len(finalBlock))
c.CryptBlocks(b.data[recordHeaderLen+explicitIVLen:], prefix)
c.CryptBlocks(b.data[recordHeaderLen+explicitIVLen+len(prefix):], finalBlock)
default:
panic("unknown cipher type")
}
}
// update length to include MAC and any block padding needed.
n := len(b.data) - recordHeaderLen //加密的时候有padding,这里需要更新; 或者带上mac的时候
b.data[3] = byte(n >> 8)
b.data[4] = byte(n)
hc.incSeq() //增加序号
return true, 0
}

解密

  • cbc模式先解密,然后再用hmac进行认证
  • AEAD模式也验证后解密
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
func (hc *halfConn) decrypt(b *block) (ok bool, prefixLen int, alertValue alert) {
// pull out payload
payload := b.data[recordHeaderLen:]
macSize := 0
if hc.mac != nil {
macSize = hc.mac.Size()
}
paddingGood := byte(255)
explicitIVLen := 0
// decrypt
if hc.cipher != nil { //已经协商出算法和密钥了
switch c := hc.cipher.(type) {
case cipher.Stream:
c.XORKeyStream(payload, payload)
case cipher.AEAD:
explicitIVLen = 8
if len(payload) < explicitIVLen {
return false, 0, alertBadRecordMAC
}
nonce := payload[:8] //序号
payload = payload[8:]
copy(hc.additionalData[:], hc.seq[:]) //序号
copy(hc.additionalData[8:], b.data[:3]) //类型,版本
n := len(payload) - c.Overhead()
hc.additionalData[11] = byte(n >> 8) //长度
hc.additionalData[12] = byte(n)
var err error
payload, err = c.Open(payload[:0], nonce, payload, hc.additionalData[:])
if err != nil {
return false, 0, alertBadRecordMAC
}
b.resize(recordHeaderLen + explicitIVLen + len(payload))
case cbcMode:
blockSize := c.BlockSize()
if hc.version >= VersionTLS11 {
explicitIVLen = blockSize
}
if len(payload)%blockSize != 0 || len(payload) < roundUp(explicitIVLen+macSize+1, blockSize) {
return false, 0, alertBadRecordMAC
}
if explicitIVLen > 0 { // tls 1.1使用随机iv
c.SetIV(payload[:explicitIVLen])
payload = payload[explicitIVLen:]
}
c.CryptBlocks(payload, payload)
if hc.version == VersionSSL30 {
payload, paddingGood = removePaddingSSL30(payload)
} else {
payload, paddingGood = removePadding(payload)
}
b.resize(recordHeaderLen + explicitIVLen + len(payload))
// note that we still have a timing side-channel in the
// MAC check, below. An attacker can align the record
// so that a correct padding will cause one less hash
// block to be calculated. Then they can iteratively
// decrypt a record by breaking each byte. See
// "Password Interception in a SSL/TLS Channel", Brice
// Canvel et al.
//
// However, our behavior matches OpenSSL, so we leak
// only as much as they do.
default:
panic("unknown cipher type")
}
}
// check, strip mac
if hc.mac != nil { //AEAD没有mac
if len(payload) < macSize {
return false, 0, alertBadRecordMAC
}
// strip mac off payload, b.data
n := len(payload) - macSize
b.data[3] = byte(n >> 8)
b.data[4] = byte(n)
b.resize(recordHeaderLen + explicitIVLen + n)
remoteMAC := payload[n:]
localMAC := hc.mac.MAC(hc.inDigestBuf, hc.seq[0:], b.data[:recordHeaderLen], payload[:n])
if subtle.ConstantTimeCompare(localMAC, remoteMAC) != 1 || paddingGood != 255 { //验证hmac
return false, 0, alertBadRecordMAC
}
hc.inDigestBuf = localMAC
}
hc.incSeq()
return true, recordHeaderLen + explicitIVLen, 0
}