持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第12天
💖SSL/TLS专栏导航💖 |
---|
💥1. SSL/TLS原理知识 |
💥2. Go源码中TLS实现 |
💥3. openssl中TLS实现 |
💥4. SSL卸载 |
💥5. SSL代理 |
💥6. SSL V.P.N |
💥7. SSL 与 IPSec |
💥8. 其他 |
获取PDF版本请搜索关键字:“TLS详解” |
TLS第二次握手
TLS第二次握手一般包括如下几个关键载荷:应该是ServerHello载荷、Certificate载荷、ServerKeyExchange载荷、ServerHelloDone载荷。这几个载荷根据实现的不同,有的分在多个报文中,有的在一个报文中传递。报文内容见下图:
在go中对应的接口为:func (hs *serverHandshakeState) doFullHandshake() error
。听听这函数的名字,就知道它没那么简单。该接口中不仅仅实现了TLS握手的第二个交互,还包括第三次TLS握手。其中TLS第二次握手部分关键代码如下:
下面对第二次握手中的关键代码进行介绍。
1. 保存tls协商报文
在这个函数中,会频繁的将交互的TLS报文保存到finishedHash中,这是为了TLS握手完毕后(Finished报文)对握手流程进行的校验。因此这里每构造一个tls载荷或者收到一个tls报文便会进行存储。
如果Server端配置时选择不校验客户端,则不会将交互报文存储。 存储报文的函数实现如下: finishedHash.write在存储报文时,还会同时计算MD5,SHA,这是TLS1.1算法要求的,TLS1.2已将此要求废除(只使用SHA算法)。从这里可以看出这个finishedHash会同时兼容多个TLS版本
2. 构造Server Hello报文
构造Server Hello报文并不是在此报文中,而是在解析Client Hello时便已经开始填充Server Hello的相关字段。
ServerHello报文中主要填充了:
- TLS Version
- Random
- Cipher Suite
注意:这里并没有填充SessionID字段,在源码中找了很久,此外通过断点调试、wireshark抓包发现sessionID字段为0。 如下图所示,TLS报文分为上下两层。上层包括握手协议、密码变更协议、告警协议、应用层数据协议;而下层统一为记录层协议。 TLS报文的封装分为两层进行:
-
上层的协议有自己的结构,每一个结构有两个基础的方法:marshal()和unmarshal(),通过这两个方法完成封装和解封装。
-
下层的记录层报文,则是通过统一的接口完成的,即writeRecord()。
ServerHello报文通过下面的接口便可以完成封装。ServerHello的完整封装是在其Marshal函数中实现的。
3. 封装Certificate载荷
在这里只是将证书取出,然后封装成certificate载荷,继而封装成handshake类型报文。
真正的解析是在processClientHello()
接口中,上面已经介绍过。 一般情况是从本地配置的证书中取出(第一个证书),存储在hs.cert中。
getCertificate
加载证书时,服务端证书配置存在三种情况:
-
未配置证书
这种情况需要从对端的ClientHello中获取证书。
-
配置一个证书
一般情况下都是配置一个证书,此时直接返回该证书即可
-
配置多个证书
此时需要根据ClientHello中的ServerName载荷或者SupportsCertificates载荷来确定最终采用的证书。
针对上述三种情况,目前我只见过第二种;其他两种情况权当扩展了。Go中实现如下: 封装完毕Certificate载荷后,还有一个OCSP载荷,目前尚未了解,后面如果有必要再补充。
4. 构造Server Key Exchange载荷
这个载荷相当的重要。它使用刚协商出的密码套件对应的处理函数来生成Key。RFC5246对该载荷描述的比较详细,如果有兴趣,可以仔细看看。ServerKeyExchange载荷发送是有前提条件的:发送的信息不足以让client生成预主密钥;通常下面这几种交换需要发送该载荷:
- DHE_DSS
- DHE_RSA
- DH_anon
ServerKeyExchange载荷格式如下: 该报文主要包括两部分:
- ECDHE相关信息
- 签名信息
在doFullHandshake()
函数中构造ServerKeyExchange的实现如下:
keyAgreement结构需要重点说明:它是客户端、服务端用于密钥协商和处理密钥交换信息的结构。
在Go源码中keyAgreement被定义为一个Interface类型。该接口包括四个函数:分别对应客户端和服务端的KeyExchange的生成和处理函数。Go中实现该keyAgreement接口的有三个:
-
🏆 rsaKA
-
🏆 ecdheRSAKA
-
🏆 ecdheECDSAKA
三个接口实现如下:
三个KeyAgreement实现最终涉及两个结构:ecdheKeyAgreement
、rsaKeyAgreement
。这两个便是密钥交换过程调用到的两个结构(二选一)。
这里先介绍基于ECDHE的密钥协商函数,也就是ecdheKeyAgreement
。它的结构定义如下:
ecdheKeyAgreement
除了上述结构定义之外,还实现了keyAgreement接口。在基于ECDHE的方式中,doFullHandshake中调用的生成KE载荷接口便是ecdheKeyAgreement
实现的generateServerKeyExchange
。
下面对ECDHE方式中生成KE载荷和处理KE载荷的函数进行介绍。
4.1 🔆🔆 generateServerKeyExchange()
此函数的主要功能包括:
- 根据本端的配置,确定采用的椭圆曲线组
- 计算ECDHE参数
- 填充server端ECDHE载荷
- 从证书中获取签名算法以及最终采用的签名算法、hash算法
- 计算Server Key Exchange的摘要
- 对摘要进行签名
- 构造完整的ServerKeyExchange载荷
函数实现如下: 下面对此函数中的重要功能进行说明。
4.2 🔆🔆ECDHE算法公私钥的生成
🔊 无论采用的哪个椭圆曲线组,DH算法的私钥都是随机生成的;然后通过私钥计算出公钥。 从代码中可以看出:ECDHE方式生成公私钥时,只需要确定采用的椭圆曲线组即可,无需其他协商参数
generateECDHEParameters
生成完毕公私钥对后,generateServerKeyExchange
将EC CurveID、公钥长度、公钥封装到握手报文的ECDH载荷中。最后通过对ECDHE载荷的摘要做一个签名。
4.3 🔆🔆ECDH载荷的签名
实际上该载荷不是直接对ECHDE载荷进行签名,而是先对ECDH载荷做hash运算,然后对hash结果使用证书中的私钥进行签名。
关于签名算法选择,RFC5246(TLS1.2)中有明确的说明:如果ClientHello中存在signature_algorithms扩展载荷,则使用扩展载荷中的算法(signature_algorithms扩展载荷中,签名算法和哈希算法必须成对存在)。7.4.3. Server Key Exchange Message
Go中签名流程如下: 这里有一点需要注意:
- ClientRandom、ServerRandom都参与了摘要的运算。
ECDH载荷和签名操作都完成中,便开始封装完整的ECDH载荷( ECDH+摘要的签名),之后generateServerKeyExchange便执行完毕。
4.4 ❓❓❓ 疑问:
虽然上面已经介绍了ServerKeyExchang载荷的流程,但是有一个疑问:协商的加密套件中有密钥交换算法、摘要算法、加密算法、认证算法,怎么感觉没有使用其中的摘要算法呢?
首先,需要说明的是:并不是所有的TLS握手报文都会包含ServerKeyExchange载荷。这一点应在前面介绍过。而密钥交换TLS中主要依靠两个方式:RSA和DH。采用RSA方式的密钥交换不需要ServerKeyExchange载荷,而采用DH方式则需要通过额外的ServerKeyExchange载荷完成密钥的交换。Go中使用方式如下:
每一个算法套件都对应一个cipherSuite结构,该结构规定了算法套件的密钥长度、摘要长度、IV长度、密钥协商算法等等。其中密钥协商算法就是上图中的第二列,主要分为两类:RSA类和ECDHE类;
🔱 采用RSA方式进行密钥交换时,由于不需要ServerKeyExchange载荷,因此rsaKA对应的generateServerKeyExchange方法为空: 🔱 采用DH方式进行密钥交换时,需要ServerKeyExchange载荷,因此ecdheECDSAKA和ecdheRSAKA对应的generateServerKeyExchange均不为空(两个实际上对应一个generateServerKeyExchange),就是上面详细介绍的那个函数。
然后,说明签名算法。DH算法无法做签名运算,只能做密钥交换;而RSA既可以做密钥交换,也可以做签名;除此之外还有DSA签名算法。因此在算法套件中,签名算法主要是RSA和DSA。当密钥交换和签名算法都采用RSA时,算法套件中第一二个算法便可以合并。这便是报文中/上面套件列表中TLS_xxx_ooo_WITH_xoxo格式并不相同的原因(WITH之前的格式)。
最后,算法套件中的签名算法和扩展载荷中的签名算法,到底使用的哪一个? 在TLS1.2中,将证书中支持的签名、ClientHello扩展载荷中的签名算法结合共同协商出采用的签名算法, 实现如下:
而算法套件中的签名算法,用来校验最终协商出的签名算法是否与加密套件中的类型一致;不一致的话则结束TLS握手流程:
if (sigType == signaturePKCS1v15 || sigType == signatureRSAPSS) != ka.isRSA {
return nil, errors.New("tls: certificate cannot be used with the selected cipher suite")
}
5. 构造Server Hello Done载荷
此载荷非常简单,不包含载荷内容,只是表明Server端的Hello报文已经结束。 代码中则是通过一个空的结构体来表示ServerHelloDone载荷。 至此,ServerHello系列报文处理完毕这些报文虽然已经封装完毕,但是并未发送,需要通过c.flush()来刷新缓存,完成报文的最终发送。