码了2000多行代码就是为了讲清楚TLS握手流程

作者 : 开心源码 本文共18736个字,预计阅读时间需要47分钟 发布时间: 2022-05-13 共236人阅读

来自公#众#号:新世界杂货铺

前言

呼,这篇文章的准备周期可谓是相当的长了!本来是想直接通过源码进行分析的,但是发现TLS握手流程调试起来非常不方便,笔者怒了,于是实现了一个极简的net.Conn接口以方便调试。码着码着,笔者哭了,由于现在这个调试Demo已经达到2000多行代码了!

image

尽管码了两千多行代码,但是目前只能够解析TLS1.3握手流程中发送的消息,因而本篇主要分析TLS1.3的握手流程。

特别提示:有想在本地调试一番的小伙伴请至文末获取本篇源码。

结论先行

鉴于本文篇幅较长,笔者决定结论先行,以助各位读者了解后文详细的分析内容。

HTTPS单向认证

单向认证用户端不需要证书,用户端只需验证服务端证书合法就可访问。

下面是笔者运行Demo打印的调试信息:

image

根据调试信息知,在TLS1.3单向认证中,总共收发数据三次,Client和Server从这三次数据中分别读取不同的信息以达到握手的目的。

注意:TLS1.3不解决ChangeCipherSpec类型的数据,而该数据在TLS1.2中是需要解决的。因本篇主要分析TLS1.3握手流程,故后续不会再提及ChangeCipherSpec,同时时序图中也会忽略此消息

笔者将调试信息转换为下述时序图,以方便各位读者了解。

image

HTTPS双向认证

双向认证不仅服务端要有证书,用户端也需要证书,只有用户端和服务端证书均合法才可继续访问。

笔者在这里特别提示,开启双向认证很简单,在笔者的Demo中取消下面代码的注释就可。

// sconf.ClientAuth = tls.RequireAndVerifyClientCert

另外,笔者在main.go同目录下留有测试用的根证书、服务端证书和用户端证书,为了保证双向认证的顺利运行请将根证书安装为受客户信任的证书。

下面是笔者运行Demo打印的调试信息:

image

同单向认证一样,笔者将调试信息转换为下述时序图。

image

双向认证和单向认证相比,Server发消息给Client时会额外发送一个certificateRequestMsgTLS13消息,Client收到此消息后会将证书信息(certificateMsgTLS13)和签名信息(certificateVerifyMsg)发送给Server。

双向认证中,Client和Server发送消息变多了,但是总的数据收发依然只有三次

总结

1、TLS1.3和TLS1.2握手流程是有区别的,这一点需要注意。

2、单向认证和双向认证中,总的数据收发仅三次,单次发送的数据中包含一个或者者多个消息。

3、clientHelloMsgserverHelloMsg未经过加密,之后发送的消息均做了加密解决。

4、Client和Server会各自计算两次密钥,计算时机分别是读取到对方的HelloMsgfinishedMsg之后。

:上述第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):

说实话,加密套件已经进入笔者的知识盲区了,其作用笔者会在下一小节讲明白,故本小节笔者直接贴出计算后的结果。

image

图中篮框部分为当前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.PreferServerCipherSuitestrue则选择Server第一个在两端均支持的加密套件,否则选择Client第一个在两端均支持的加密套件。笔者通过Debug得到两端均支持的加密套件id为4865(其常量为tls.TLS_AES_128_GCM_SHA256),介绍见下图:

image

上图中的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, aeadAESGCMTLS13, crypto.SHA384},}

结合前面的Debug信息知,hs.suitecipherSuiteTLS13结构体的变量且其值为cipherSuitesTLS13切片的第一个。cipherSuiteTLS13结构体定义如下:

type cipherSuiteTLS13 struct {    id     uint16    keyLen int    aead   func(key, fixedNonce []byte) aead    hash   crypto.Hash}

至此,Server已经计算出双端均支持的加密套件,Server通过设置cipherSuite将双端均支持的加密套件告知Client:

hs.hello.cipherSuite = hs.suite.idhs.transcript = hs.suite.hash.New()

在后续计算密钥时需要对Client和Server之间的所有消息计算Hash摘要。根据前面计算出的加密套件知,本篇中计算消息摘要的Hash算法为SHA256,此算法的实现赋值给hs.transcript变量,后续计算消息摘要时均通过该变量实现。

Server计算双端均支持的密钥交换协议以及对应的公钥

clientHelloMsg.keyShares变量记录着Client支持的曲线Id以及对应的公钥。Server通过比照本地支持的曲线Id计算出双端均支持的密钥交换协议。根据前面Client发送HelloMsg这一小节的内容以及笔者实际调试的结果,双端均支持的曲线为Curve25519

Server计算出双端均支持的曲线后,调用generateECDHEParameters方法得到对应密钥交换协议的实现,即Curve25519密钥交换协议。

Curve25519是椭圆曲线迪菲-赫尔曼(Elliptic-curve Diffie–Hellman ,缩写为ECDH)密钥交换方案之一,同时也是最快的ECC(Elliptic-curve cryptography)曲线之一。

ECDH可以为Client和Server在不安全的通道上为双方建立共享密钥,并且Client和Server需要各自持有一组椭圆曲线公私密钥对。当Client和Server需要建立共享密钥时仅需要公布各自的公钥,Client和Server通过对方的公钥以及自己的私钥就可计算出相等的密钥。假如公钥被第三方截获也无关紧要,由于第三方没有私钥无法计算出共享密钥除非第三方能够处理椭圆曲线Diffie–Hellman问题。ECDHEECDH的一个变种,其区别仅仅是私钥和公钥在每次建立共享密钥时均需重新生成(以上为笔者对维基百科中ECDH的了解)。

ECDHE有了肯定的了解后,我们现在看一下generateECDHEParameters函数中的部分源码:

func generateECDHEParameters(rand io.Reader, curveID CurveID) (ecdheParameters, error) {    if curveID == X25519 {        privateKey := make([]byte, curve25519.ScalarSize)        if _, err := io.ReadFull(rand, privateKey); err != nil {            return nil, err        }        publicKey, err := curve25519.X25519(privateKey, curve25519.Basepoint)        if err != nil {            return nil, err        }        return &x25519Parameters{privateKey: privateKey, publicKey: publicKey}, nil    }  // 此处省略代码}

每次调用generateECDHEParameters函数时均会生成一组新的椭圆曲线公私密钥对。clientHelloMsg.keyShares变量存有Client的公钥,因而Server已经可以计算共享密钥:

params, err := generateECDHEParameters(c.config.rand(), selectedGroup)if err != nil {  c.sendAlert(alertInternalError)  return err}hs.hello.serverShare = keyShare{group: selectedGroup, data: params.PublicKey()}hs.sharedKey = params.SharedKey(clientKeyShare.data) // 共享密钥

上述代码中Server已经计算出共享密钥,之后可以通过此密钥派生出其余密钥为数据加密。Client由于无Server的公钥还无法计算出共享密钥,所以Server通过设置serverShare变量告知Client服务端的公钥。

至此,Server对Client发来的helloMsg已经解决完毕。笔者在这里额外提示一句,clientHelloMsgserverHelloMsg中依然有Client和Server生成的随机数,但是在TLS1.3中这两个随机数已经和密钥交换无关了。

小结:本节详情了Server读取clientHelloMsg后会计算双端支持的TLS版本以及双端支持的加密套件和密钥交换协议,同时还详情了共享密钥的生成以及ECDH的概念。

选择合适的证书以及签名算法

在Server选择和当前Client匹配的证书前其实还有关于预共享密钥模式的解决,该模式需要实现ClientSessionCache接口,鉴于其不影响握手流程的分析,故本篇不探讨预共享密钥模式。

一个Server可能给多个Host提供服务,因而Server可能持有多个证书,那么选择一个和当前Client匹配的证书是十分必要的,其实现逻辑参见(*Config).getCertificate方法。本篇中的Demo只有一个证书,故该方法会直接返回此证书。

证书中是包含公钥的,不同的公钥支持的签名算法是不同的,在本例中Server支持的签名算法和最终双端均支持的签名算法见下面的Debug结果:

image

上图中红框部分为Server支持的签名算法,蓝框为选定的双端均支持的签名算法。

小结:本节主要详情了Server选择匹配当前Client的证书和签名算法。

计算握手阶段的密钥以及发送Server的参数

在这个阶段Server会将serverHelloMsg写入缓冲区,写完之后再写入一个ChangeCipherSpec(TLS1.3不会解决此消息)消息,需要注意的是serverHelloMsg未进行加密发送。

计算握手阶段的密钥

前面提到过计算密钥需要计算消息摘要:

hs.transcript.Write(hs.clientHello.marshal())hs.transcript.Write(hs.hello.marshal()) // hs.hello为serverHelloMsg

上述代码中hs.transcript在前面已经提到过是SHA256Hash算法的一种实现。下面我们逐渐分析源码中Server第一次计算密钥的过程。

首先,派生出handshakeSecret

earlySecret := hs.earlySecret if earlySecret == nil {  earlySecret = hs.suite.extract(nil, nil)}hs.handshakeSecret = hs.suite.extract(hs.sharedKey, hs.suite.deriveSecret(earlySecret, "derived", nil))

earlySecret和预共享密钥有关,因本篇不涉及预共享密钥,故earlySecretnil。此时,earlySecret会通过加密套件派生出一个密钥。

// extract implements HKDF-Extract with the cipher suite hash.func (c *cipherSuiteTLS13) extract(newSecret, currentSecret []byte) []byte {    if newSecret == nil {        newSecret = make([]byte, c.hash.Size())    }    return hkdf.Extract(c.hash.New, newSecret, currentSecret)}

上述代码中HDKF是一种基于哈希消息身份验证的密钥派生算法,其两个主要用途分别为:一、从较大的随机源中提取更加均匀和随机的密钥;二、将已经正当的随机输入(例如共享密钥)扩展为更大的密码独立输出,从而将共享密钥派生出多个密钥(以上为笔者对维基百科中HKDF的了解)。

上述代码中hs.suite.deriveSecret方法笔者就不列出其源码了,该方法最终会调用hkdf.Expand方法进行密钥派生。

此时再次回顾hs.handshakeSecret的生成正是HKDF算法基于sharedKeyearlySecret计算的结果。

而后,通过handshakeSecret和消息摘要派生出一组密钥。

clientSecret := hs.suite.deriveSecret(hs.handshakeSecret,    clientHandshakeTrafficLabel, hs.transcript)c.in.setTrafficSecret(hs.suite, clientSecret)serverSecret := hs.suite.deriveSecret(hs.handshakeSecret,    serverHandshakeTrafficLabel, hs.transcript)c.out.setTrafficSecret(hs.suite, serverSecret)

上述代码中clientHandshakeTrafficLabelserverHandshakeTrafficLabel为常量,其值分别为c hs traffics hs traffichs.suite.deriveSecret方法会在内部调用hs.transcript.Sum(nil)计算出消息的摘要信息,所以clientSecretserverSecretHKDF算法基于handshakeSecret和两个常量以及Server和Client已经发送的消息的摘要派生出的密钥。

clientSecret在服务端用于对收到的数据进行解密,serverSecret在服务端对要发送的数据进行加密。c.inc.out同其语义一样,分别用于解决收到的数据和要发送的数据。

下面看看笔者对setTrafficSecret方法的Debug结果:

image

上图中trafficKey方法使用HKDF算法对密钥进行了再次派生,笔者就不再对其开展。这里需要关注的是红框部分,aes-gcm是一种AEAD加密。

单纯的对称加密算法,其解密步骤是无法确认密钥能否正确的。也就是说,加密后的数据可以用任何密钥执行解密运算,得到一组疑似原始数据,然而并不知道密钥能否是正确,也不知道解密出来的原始数据能否正确。因而,需要在单纯的加密算法之上,加上一层验证手段,来确认解密步骤能否正确,这就是AEAD

至此,Server在握手阶段的密钥生成结束,此阶段之后发送的消息(即serverHelloMsgChangeCipherSpec之后的消息),均通过aes-gcm算法加密。

最后回顾一下加密套件的作用:

1、提供消息摘要的Hash算法。

2、提供加解密的AEAD算法。

最后再顺便提一嘴,笔者Demo中parse.go文件的processMsg方法在解决serverHelloMsg时有计算握手阶段密钥的极简实现。

支持的HTTP协议

Client通过clientHelloMsg.alpnProtocols告知Server用户端支持的HTTP协议,Server通过比照本地支持的HTTP协议,最终选择双端均支持的协议并构建encryptedExtensionsMsg消息告知Client

encryptedExtensions := new(encryptedExtensionsMsg)if len(hs.clientHello.alpnProtocols) > 0 {  if selectedProto, fallback := mutualProtocol(hs.clientHello.alpnProtocols, c.config.NextProtos); !fallback {    encryptedExtensions.alpnProtocol = selectedProto    c.clientProtocol = selectedProto  }}hs.transcript.Write(encryptedExtensions.marshal())

hs.clientHello.alpnProtocols的数据来源为用户端的tls.Config.NextProtos。在笔者的Demo中,Client和Server均支持h2http1.1这两种协议。

这里顺便强调一下,Client或者者Server在获取到对方的helloMsg之后接受/发送的消息均会调用hs.transcript.Write方法,以便计算密钥时可以快速计算消息摘要。

小结

1、本节探讨了握手阶段的密钥生成流程:对消息摘要,而后用HKDF算法对共享密钥和消息摘要派生密钥,最后通过加密套件返回AEAD算法的实现。

2、确认了加密套件的作用。

3、计算两端均支持的HTTP协议。

发送Server证书以及签名

此阶段主要涉及三个消息,分别是certificateRequestMsgTLS13certificateMsgTLS13certificateVerifyMsg

其中certificateRequestMsgTLS13仅在双向认证时才发送给Client,单向认证时Server不发送此消息。这里也再次印证了前面单向认证和双向认证时序图中Server发送的消息数量不一致的起因。

certificateMsgTLS13消息的主体是Server的证书这个没什么好说的,下面着重分析一下certificateVerifyMsg

私钥签名

首先,构建certificateVerifyMsg并设置其签名算法。

certVerifyMsg := new(certificateVerifyMsg)certVerifyMsg.hasSignatureAlgorithm = true // 没有签名算法无法签名,所以直接写true没毛病certVerifyMsg.signatureAlgorithm = hs.sigAlg

上述代码中hs.sigAlg选择合适的证书以及签名算法小节选择的签名算法。

而后,通过签名算法计算签名类型以及签名hash,并构建签名选项。以下为笔者Debug结果:

image

由上图知,签名类型为signatureRSAPSS,签名哈希算法为SHA256signedMessage的作用是将消息的摘要和serverSignatureContext(值为TLS 1.3, server CertificateVerify\x00)常量按照固定格式构建为待签名数据。

最后,计算签名并发送消息。

sig, err := hs.cert.PrivateKey.(crypto.Signer).Sign(c.config.rand(), signed, signOpts)if err != nil {  // 省略代码  return errors.New("tls: failed to sign handshake: " + err.Error())}certVerifyMsg.signature = sighs.transcript.Write(certVerifyMsg.marshal())

特别提示,私钥加密公钥解密称之为签名。

小结:本节主要详情了此阶段会发送的三种消息,以及Server签名的过程。

发送finishedMsg并再次计算密钥

发送finishedMsg

finishedMsg的内容非常简单,仅一个字段:

finished := &finishedMsg{  verifyData: hs.suite.finishedHash(c.out.trafficSecret, hs.transcript),}

verifyData通过加密套件的finishedHash计算得出,下面我们看看finishedHash的内容:

func (c *cipherSuiteTLS13) finishedHash(baseKey []byte, transcript hash.Hash) []byte {    finishedKey := c.expandLabel(baseKey, "finished", nil, c.hash.Size())    verifyData := hmac.New(c.hash.New, finishedKey)    verifyData.Write(transcript.Sum(nil))    return verifyData.Sum(nil)}

HMAC是一种利用密码学中的散列函数来进行消息认证的一种机制,所能提供的消息认证包括两方面内容(此内容摘自百度百科):

消息完整性认证:能够证实消息内容在传送过程没有被修改。

信源身份认证:由于通信双方共享了认证的密钥,接收方能够认证发送该数据的信源与所声称的一致,即能够可靠地确认接收的消息与发送的一致。

上述代码中,c.expandLabel最种会调用hkdf.Expand派生出新的密钥。最后用新的密钥以及消息摘要通过HMAC算法计算出verifyData

收到finishedMsg一方通过同样的方式在本地计算出verifyData',假如verifyData'verifyData相等,则证实此消息未被修改且来源可信。

再次计算密钥

本次计算密钥的过程和前面计算密钥的流程类似,所以直接上代码:

hs.masterSecret = hs.suite.extract(nil,    hs.suite.deriveSecret(hs.handshakeSecret, "derived", nil))hs.trafficSecret = hs.suite.deriveSecret(hs.masterSecret,    clientApplicationTrafficLabel, hs.transcript)serverSecret := hs.suite.deriveSecret(hs.masterSecret,    serverApplicationTrafficLabel, hs.transcript)c.out.setTrafficSecret(hs.suite, serverSecret)

首先,利用前文已经生成的handshakeSecret 再次派生出masterSecret,而后再从masterSecret派生出trafficSecretserverSecret,最后调用c.out.setTrafficSecret(hs.suite, serverSecret)计算出Server发送数据时的AEAD加密算法。

需要注意的是,此时利用serverSecret生成的AEAD加密算法会用于握手结束后对要发送的业务数据进行加密。

此阶段结束后,Server会调用c.flush()方法,将前面提到的消息一次性发送给Client。

小结

1、本节详情了finishedMsg的生成过程,其中finishedMsg.verifyData通过HMAC算法计算得出。

2、finishedMsg的作用是确保握手过程中发送的消息未被篡改,且数据来源可信。

3、计算Server发送业务数据时的加密密钥。

Client读消息&发送消息

Client读到serverHelloMsg之后会读取服务端支持的TLS版本并和本地支持的版本做比照,前文已经提到过服务端支持的TLS版本是TLS1.3,因而Client也进入TLS1.3握手流程。

读取serverHelloMsg并计算密钥

Client进入TLS1.3握手流程后,有一系列的检查逻辑,这些逻辑比较长而且笔者也不需要考虑这些异常,因而笔者化繁为简,在下面列出关键逻辑:

selectedSuite := mutualCipherSuiteTLS13(hs.hello.cipherSuites,    hs.serverHello.cipherSuite) // 结合Server支持的加密套件选择双端均支持的加密套件hs.suite = selectedSuitehs.transcript = hs.suite.hash.New()hs.transcript.Write(hs.hello.marshal()) // hs.hello为clientHelloMsghs.transcript.Write(hs.serverHello.marshal())

上面这一段代码逻辑和Server解决加密套件以及通过加密套件构建消息摘要算法的实现逻辑相对应,因而笔者不再过多赘述。

下面我们看一下计算握手阶段的密钥以及masterSecret的生成:

sharedKey := hs.ecdheParams.SharedKey(hs.serverHello.serverShare.data)earlySecret := hs.earlySecretif !hs.usingPSK {  earlySecret = hs.suite.extract(nil, nil)}handshakeSecret := hs.suite.extract(sharedKey,    hs.suite.deriveSecret(earlySecret, "derived", nil)) // 通过共享密钥派生出handshakeSecretclientSecret := hs.suite.deriveSecret(handshakeSecret,    clientHandshakeTrafficLabel, hs.transcript) // 通过handshakeSecret派生出clientSecretc.out.setTrafficSecret(hs.suite, clientSecret)serverSecret := hs.suite.deriveSecret(handshakeSecret,    serverHandshakeTrafficLabel, hs.transcript) // 通过handshakeSecret派生出serverSecretc.in.setTrafficSecret(hs.suite, serverSecret)hs.masterSecret = hs.suite.extract(nil,    hs.suite.deriveSecret(handshakeSecret, "derived", nil)) // 通过handshakeSecret派生出masterSecret

这里需要提一嘴的是hs.ecdheParams,该值为Client发送HelloMsg这一小节调用generateECDHEParameters函数生成的params。其余逻辑和Server生成握手阶段的密钥保持一致,硬要说不同的话也就只有masterSecret生成的阶段不同。

最后,clientSecret在用户端用于对要发送的数据进行加密,serverSecret在用户端对收到的数据进行解密。

小结:本节梳理了用户端解决serverHelloMsg的逻辑和生成握手阶段密钥的逻辑。

解决Server发送的参数

在用户端需要解决的Server参数只有一个encryptedExtensionsMsg消息。而且解决逻辑也十分简单:

msg, err := c.readHandshake()encryptedExtensions, ok := msg.(*encryptedExtensionsMsg)hs.transcript.Write(encryptedExtensions.marshal())c.clientProtocol = encryptedExtensions.alpnProtocol

假如用户端读取到encryptedExtensionsMsg消息,则直接将Server支持的HTTP协议赋值给c.clientProtocol。在之后的HTTP请求中会根据TLS握手状态以及服务端能否支持h2决定能否将本次请求更新为http2

验证证书和签名

本小节依然继续解决Server发送的消息,主要包含certificateRequestMsgTLS13certificateMsgTLS13certificateVerifyMsg,这三个消息均和证书相关。

首先,解决certificateRequestMsgTLS13消息,仅在双向认证时,服务端才发送此消息。在本阶段的解决逻辑也很简单,读取该消息并记录。

msg, err := c.readHandshake()certReq, ok := msg.(*certificateRequestMsgTLS13)if ok {  hs.transcript.Write(certReq.marshal())  hs.certReq = certReq  msg, err = c.readHandshake()}

其次,解决certificateMsgTLS13消息,该消息中主要包含证书信息,Client在获取到证书信息后要校验证书能否过期以及能否可信任。

if err := c.verifyServerCertificate(certMsg.certificate.Certificate); err != nil {  return err}

c.verifyServerCertificate的内部逻辑假如各位读者有兴趣可以下载Demo调试一番,笔者在这里就不对该方法做深入的开展和分析了。

最后,解决certificateVerifyMsg消息。前面在解决certificateMsgTLS13时已经验证了证书可信任或者者Client可以忽略不受信任的证书,但是Client仍无法确信提供这个证书的服务器能否持有该证书,而验证签名的意义就在于确保该服务的确持有该证书。

在Server发送certificateVerifyMsg消息时已经使用了证书对应的私钥对需要签名的数据进行签名,用户端利用证书的公钥解密该签名并和本地的待签名数据做比照以确保服务端的确持有该证书。

// 根据签名算法返回对应的算法类型和hash算法sigType, sigHash, err := typeAndHashFromSignatureScheme(certVerify.signatureAlgorithm)signed := signedMessage(sigHash, serverSignatureContext, hs.transcript)if err := verifyHandshakeSignature(sigType, c.peerCertificates[0].PublicKey,    sigHash, signed, certVerify.signature); err != nil {  c.sendAlert(alertDecryptError)  return errors.New("tls: invalid signature by the server certificate: " + err.Error())}

typeAndHashFromSignatureScheme函数和signedMessage函数在前文已经提到过,因而不再做重复叙述。

verifyHandshakeSignature函数的内部实现涉及到非对称加密算法的加解密,因笔者的知识有限,的确无法做更进一步的分析,在这里给各位读者道个歉~

小结:在这一小节简单详情了用户端证书的验证以及签名的验证。

解决finishedMsg并再次计算密钥

用户端对证书签名验证通过后,接下来还需要验证消息的完整性。

finished, ok := msg.(*finishedMsg)expectedMAC := hs.suite.finishedHash(c.in.trafficSecret, hs.transcript)if !hmac.Equal(expectedMAC, finished.verifyData) {  c.sendAlert(alertDecryptError)  return errors.New("tls: invalid server finished hash")}

finishedHash方法说明请参考发送finishedMsg并再次计算密钥这一小节。

只有当用户端计算的expectedMACfinishedMsg.verifyData一致时才可继续后续操作,即用户端二次计算密钥。

hs.trafficSecret = hs.suite.deriveSecret(hs.masterSecret,    clientApplicationTrafficLabel, hs.transcript)serverSecret := hs.suite.deriveSecret(hs.masterSecret,    serverApplicationTrafficLabel, hs.transcript)c.in.setTrafficSecret(hs.suite, serverSecret)

二次计算密钥时分别派生出trafficSecretserverSecret两个密钥。

需要注意的是,此时利用serverSecret生成的AEAD加密算法会用于握手结束后对收到的业务数据进行解密。

至此,Server发送给用户端的消息已经一律解决完毕。

小结:本节主要详情了用户端通过HMAC算法确保收到的消息未被篡改以及二次计算密钥。

Client发送最后的消息

用户端已经验证了服务端消息的完整性,但是服务端还未验证用户端消息的完整性,因而用户端还需要发送最后一次数据给服务端。

首先判断能否需要发送证书给Server:

if hs.certReq == nil {  return nil}certMsg := new(certificateMsgTLS13)// 此处省略代码certVerifyMsg := new(certificateVerifyMsg)certVerifyMsg.hasSignatureAlgorithm = true// 此处省略代码

根据验证证书和签名这一小节的形容,假如服务端要求用户端发送证书则hs.certReq不为nil。

certificateMsgTLS13的主体也是证书,该证书的来源为用户端tls.Config配置的证书,在本例中用户端配置证书逻辑如下:

tlsConf.NextProtos = append(tlsConf.NextProtos, "h2", "http/1.1")tlsConf.Certificates = make([]tls.Certificate, 1)if len(certFile) > 0 && len(keyFile) > 0 {  var err error  tlsConf.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile)  if err != nil {    return nil, err  }}

既然要发送证书给服务端,那么同服务端逻辑一样也需要发送certificateVerifyMsg提供消息签名的信息。用户端签名逻辑和服务端签名逻辑一致,因而笔者不再赘述。

最后,用户端需要发送finishedMsg给服务端:

finished := &finishedMsg{  verifyData: hs.suite.finishedHash(c.out.trafficSecret, hs.transcript),}hs.transcript.Write(finished.marshal())c.out.setTrafficSecret(hs.suite, hs.trafficSecret)

需要注意的是hs.trafficSecret在第二次计算密钥时就已经被赋值,当finishedMsg发送后,利用hs.trafficSecret生成的AEAD加密算法会对用户端要发送的业务数据进行加密。

至此,用户端的握手流程一律完成。

小结

1、假如服务端要求用户端发送证书,则用户端会发送certificateMsgTLS13certificateVerifyMsg消息

2、发送finishedMsg消息并设置发送业务数据时的密钥信息。

Server读Client最后的消息

首先,服务端在TLS握手的最后阶段,会先判断能否要求用户端发送证书,假如要求用户端发送证书则解决用户端发送的certificateMsgTLS13certificateVerifyMsg消息。服务端解决certificateMsgTLS13certificateVerifyMsg消息的逻辑和用户端解决这两个消息的逻辑相似。

其次,读取用户端发送的finishedMsg, 并验证消息的完整性,验证逻辑和用户端验证finishedMsg逻辑一致。

最后,设置服务端读取业务数据时的加密信息:

c.in.setTrafficSecret(hs.suite, hs.trafficSecret)

hs.trafficSecret在服务端第二次计算加密信息时就已经赋值,当读完用户端发送的finishedMsg之后再执行此步骤是为了避免无法解密用户端发送的握手信息。

至此,服务端的握手流程一律完成。

握手完成之后

完成上述流程后,笔者还想试试看能不能从握手过程获取的密钥信息对业务数据进行解密。说干就干,下面是笔者在TLS握手完成之后用Client连接发送了一条消息的代码。

// main.go 握手完成之后,client发送了一条数据client.Write([]byte("点赞关注:新世界杂货铺"))

下面是运行Demo后的输出截图:

image

图中红色箭头部分为在Internet中真实传输的数据,蓝色箭头部分为其解密结果。

一点感慨

关于TLS握手流程的文章笔者想写很久了,现在总算得偿所愿。笔者不敢保证把TLS握手过程的每一个细节都形容清楚,所以假如中间有什么问题还请各位读者及时指出,大家相互学习。

写到这里时笔者的内心也略有忐忑,毕竟这中间涉及了很多密码学相关的知识,而在笔者各种疯狂查资料期间发现国内具备权威性的文章还是太少。像ECDH之类的关键词在百度百科都没有收录,果然维基百科才是爸爸呀。

最后一点感概是关于Go中io.Readerio.Writer这两个接口的,不得不说这两个接口的设计真的很简单但是真的非常通用。笔者的Demo正是基于这两个接口实现,否则笔者的心愿很难完成。

挖坑

在上一篇文章中,笔者给了一条彩蛋——“下一期TLS/SSL握手流程敬请期待”。哇,这可真的是自己坑自己了,本篇文章未完成之前,笔者愣是断更了也没敢发别的文章。果然自己作的死,哭着也要作完。

有了前车之鉴,笔者决定以后不再放彩蛋,而是挖坑(填坑时间待定?):本篇中主要详情了TLS1.3的握手流程,那么TLS1.2也快了~

最后,衷心希望本文能够对各位读者有肯定的帮助。

  1. 写本文时, 笔者所用go版本为: go1.15.2
  2. 文章中所用完整例子: Isites/go-coder/blob/master/http2/tls/main.go
说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 码了2000多行代码就是为了讲清楚TLS握手流程

发表回复