乐趣区

关于后端:码了2000多行代码就是为了讲清楚TLS握手流程

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

前言

呼,这篇文章的筹备周期堪称是相当的长了!本来是想间接通过源码进行剖析的,然而发现 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、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):

说实话,加密套件曾经进入笔者的常识盲区了,其作用笔者会在下一大节讲明确,故本大节笔者间接贴出计算后的后果。

图中篮框局部为以后 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),详情见下图:

上图中的 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.id
hs.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 后果:

上图中红框局部为 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 后果:

上图中 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 后果:

由上图知,签名类型为 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 = sig
hs.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 = selectedSuite
hs.transcript = hs.suite.hash.New()
hs.transcript.Write(hs.hello.marshal()) // hs.hello 为 clientHelloMsg
hs.transcript.Write(hs.serverHello.marshal())

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

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

sharedKey := hs.ecdheParams.SharedKey(hs.serverHello.serverShare.data)
earlySecret := hs.earlySecret
if !hs.usingPSK {earlySecret = hs.suite.extract(nil, nil)
}
handshakeSecret := hs.suite.extract(sharedKey,
    hs.suite.deriveSecret(earlySecret, "derived", nil)) // 通过共享密钥派生出 handshakeSecret

clientSecret := hs.suite.deriveSecret(handshakeSecret,
    clientHandshakeTrafficLabel, hs.transcript) // 通过 handshakeSecret 派生出 clientSecret
c.out.setTrafficSecret(hs.suite, clientSecret)
serverSecret := hs.suite.deriveSecret(handshakeSecret,
    serverHandshakeTrafficLabel, hs.transcript) // 通过 handshakeSecret 派生出 serverSecret
c.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 后的输入截图:

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

一点感叹

对于 TLS 握手流程的文章笔者想写很久了,当初总算得偿所愿。笔者不敢保障把 TLS 握手过程的每一个细节都形容分明,所以如果两头有什么问题还请各位读者及时指出,大家互相学习。

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

最初一点感概是对于 Go 中 io.Reader io.Writer这两个接口的,不得不说这两个接口的设计真的很简略然而真的十分通用。笔者的 Demo 正是基于这两个接口实现,否则笔者的宿愿很难实现。

挖坑

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

有了前事不忘; 后事之师,笔者决定当前不再放彩蛋,而是挖坑(填坑工夫待定????):本篇中次要介绍了 TLS1.3 的握手流程,那么 TLS1.2 也快了~

最初,衷心希望本文可能对各位读者有肯定的帮忙。

  1. 写本文时,笔者所用 go 版本为: go1.15.2
  2. 文章中所用残缺例子:https://github.com/Isites/go-…
退出移动版