来自公众号:新世界杂货铺
文章目录
前言
呼,这篇文章的准备周期可谓是相当的长了!原本是想直接通过源码进行分析的,但是发现TLS握手流程调试起来非常不方便,笔者怒了,于是实现了一个极简的net.Conn
接口以方便调试。码着码着,笔者哭了,因为现在这个调试Demo已经达到2000多行代码了!
虽然码了两千多行代码,但是目前只能够解析TLS1.3握手流程中发送的消息,因此本篇主要分析TLS1.3的握手流程。
特别提醒:有想在本地调试一番的小伙伴请至文末获取本篇源码。
结论先行
鉴于本文篇幅较长,笔者决定结论先行,以助各位读者理解后文详细的分析内容。
HTTPS单向认证
单向认证客户端不需要证书,客户端只要验证服务端证书合法即可访问。
下面是笔者运行Demo打印的调试信息:
根据调试信息知,在TLS1.3单向认证中,总共收发数据三次,Client和Server从这三次数据中分别读取不同的信息以达到握手的目的。
注意:TLS1.3不处理ChangeCipherSpec
类型的数据,而该数据在TLS1.2中是需要处理的。因本篇主要分析TLS1.3握手流程,故后续不会再提及ChangeCipherSpec
,同时时序图中也会忽略此消息。
笔者将调试信息转换为下述时序图,以方便各位读者理解。
HTTPS双向认证
双向认证不仅服务端要有证书,客户端也需要证书,只有客户端和服务端证书均合法才可继续访问。
笔者在这里特别提醒,开启双向认证很简单,在笔者的Demo中取消下面代码的注释即可。
// sconf.ClientAuth = tls.RequireAndVerifyClientCert
另外,笔者在main.go
同目录下留有测试用的根证书、服务端证书和客户端证书,为了保证双向认证的顺利运行请将根证书安装为受用户信任的证书。
下面是笔者运行Demo打印的调试信息:
同单向认证一样,笔者将调试信息转换为下述时序图。
双向认证和单向认证相比,Server发消息给Client时会额外发送一个certificateRequestMsgTLS13
消息,Client收到此消息后会将证书信息(certificateMsgTLS13
)和签名信息(certificateVerifyMsg
)发送给Server。
双向认证中,Client和Server发送消息变多了,但是总的数据收发仍然只有三次。
总结
1、TLS1.3和TLS1.2握手流程是有区别的,这一点需要注意。
2、单向认证和双向认证中,总的数据收发仅三次,单次发送的数据中包含一个或者多个消息。
3、clientHelloMsg
和serverHelloMsg
未经过加密,之后发送的消息均做了加密处理。
4、Client和Server会各自计算两次密钥,计算时机分别是读取到对方的HelloMsg
和finishedMsg
之后。
注:上述第3点和第4点分析过程详见后文。
Client发送HelloMsg
在TLS握手过程中的第一步是Client发送HelloMsg,所以针对TLS握手流程的分析也从这一步开始。
Server对于Client的基本信息了解完全依赖于Client主动告知Server,而其中比较关键的信息分别是客户端支持的TLS版本
、客户端支持的加密套件(cipherSuites)
、客户端支持的签名算法
和客户端支持的密钥交换协议以及其对应的公钥
。
客户端支持的TLS版本:
客户端支持的TLS版本主要通过tls包中(*Config).supportedVersions
方法计算。对TLS1.3来说默认支持的TLS版本如下:
var supportedVersions = []uint16{
VersionTLS13,
VersionTLS12,
VersionTLS11,
VersionTLS10,
}
在发起请求时如果用户手动设置了tls.Config
中的MaxVersion
或者MinVersion
,则客户端支持的TLS版本会发生变化。
例如发起请求时,设置了conf.MaxVersion = tls.VersionTLS12
,此时(*Config).supportedVersions
返回的版本为:
[]uint16{
VersionTLS12,
VersionTLS11,
VersionTLS10,
}
ps: 如果有兴趣的小伙伴可以在克隆笔者的demo后手动设置Config.MaxVersion,设置后可以调试TLS1.2的握手流程。
客户端支持的加密套件(cipherSuites):
说实话,加密套件已经进入笔者的知识盲区了,其作用笔者会在下一小节讲明白,故本小节笔者直接贴出计算后的结果。
图中篮框部分为当前Client支持加密套件Id,红框部分为计算逻辑。
客户端支持的签名算法:
客户端支持的签名算法,仅在客户端支持的最大TLS版本大于等于TLS1.2时生效。此时客户端支持的签名算法如下:
var supportedSignatureAlgorithms = []SignatureScheme{
PSSWithSHA256,
ECDSAWithP256AndSHA256,
Ed25519,
PSSWithSHA384,
PSSWithSHA512,
PKCS1WithSHA256,
PKCS1WithSHA384,
PKCS1WithSHA512,
ECDSAWithP384AndSHA384,
ECDSAWithP521AndSHA512,
PKCS1WithSHA1,
ECDSAWithSHA1,
}
客户端支持的密钥交换协议及其对应的公钥:
这一块儿逻辑仅在客户端支持的最大TLS版本是TLS1.3时生效。
if hello.supportedVersions[0] == VersionTLS13 {
hello.cipherSuites = append(hello.cipherSuites, defaultCipherSuitesTLS13()...)
curveID := config.curvePreferences()[0]
if _, ok := curveForCurveID(curveID); curveID != X25519 && !ok {
return nil, nil, errors.New("tls: CurvePreferences includes unsupported curve")
}
params, err = generateECDHEParameters(config.rand(), curveID)
if err != nil {
return nil, nil, err
}
hello.keyShares = []keyShare{
{
group: curveID, data: params.PublicKey()}}
}
上述代码中,方法config.curvePreferences
的逻辑为:
var defaultCurvePreferences = []CurveID{
X25519, CurveP256, CurveP384, CurveP521}
func (c *Config) curvePreferences() []CurveID {
if c == nil || len(c.CurvePreferences) == 0 {
return defaultCurvePreferences
}
return c.CurvePreferences
}
在本篇中,笔者未手动设置优先可供选择的曲线,故curveID
的值为X25519
。
上述代码中,generateECDHEParameters
函数的作用是根据曲线Id生成一种椭圆曲线密钥交换协议的实现。
如果客户端支持的最大TLS版本是TLS1.3时,会为Client支持的加密套件增加TLS1.3默认的加密套件,同时还会选择Curve25519密钥交换协议生成keyShare
。
小结:本节介绍了在TLS1.3中Client需要告知Server客户端支持的TLS版本号、客户端支持的加密套件、客户端支持的签名算法和客户端支持的密钥交换协议。
Server读HelloMsg&发送消息
Server读到clientHelloMsg
之后会根据客户端支持的TLS版本和本地支持的TLS版本做对比,得到Client和Server均支持的TLS版本最大值,该值作为后续继续通信的标准。本篇中Client和Server都支持TLS1.3,因此Server进入TLS1.3的握手流程。
处理clientHelloMsg
Server进入TLS1.3握手流程之后,还需要继续处理clientHelloMsg,同时构建serverHelloMsg
。
Server支持的TLS版本:
进入TLS1.3握手流程之前,Server已经计算出两端均支持的TLS版本,但是Client还无法得知Server支持的TLS版本,因此开始继续处理clientHelloMsg时,Server将已经计算得到的TLS版本赋值给supportedVersion
以告知客户端。
// client读取到serverHelloMsg后,通过读取此字段计算两端均支持的TLS版本
hs.hello.supportedVersion = c.vers
Server计算两端均支持的加密套件:
clientHelloMsg
中含有Client支持的加密套件信息,Server读取该信息并和本地支持的加密套件做对比计算出两端均支持的加密套件。
这里需要注意的是,如果Server的tls.Config.PreferServerCipherSuites
为true
则选择Server第一个在两端均支持的加密套件,否则选择Client第一个在两端均支持的加密套件。笔者通过Debug得到两端均支持的加密套件id为4865
(其常量为tls.TLS_AES_128_GCM_SHA256
),详情见下图:
上图中的mutualCipherSuiteTLS13
函数会从cipherSuitesTLS13
变量中选择匹配的加密套件。
var cipherSuitesTLS13 = []*cipherSuiteTLS13{
{
TLS_AES_128_GCM_SHA256, 16, aeadAESGCMTLS13, crypto.SHA256},
{
TLS_CHACHA20_POLY1305_SHA256, 32, aeadChaCha20Poly1305, crypto.SHA256},
{
TLS_AES_256_GCM_SHA384, 32, aeadA