Go中TLS源码学习(5)之Server端第二次TLS握手

631 阅读8分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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实现最终涉及两个结构:ecdheKeyAgreementrsaKeyAgreement。这两个便是密钥交换过程调用到的两个结构(二选一)。

这里先介绍基于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()来刷新缓存,完成报文的最终发送。