grpc-go源码剖析九十三之数据帧发送阶段来分析grpc框架加密的原理?

本小节主要是介绍使用tls链路传输时,是如何对数据帧进行加密的?

我们以客户端一侧发送数据帧为例。

假设,将创建好的数据帧交由帧发送器进行发送,看看是如何对数据帧进行加密的?

1、分析入口?帧发送器里的processData

分析入口是grpc-go/internal/transport/controlbuf.go文件中的processData方法里:

func (l *loopyWriter) processData() (bool, error) {
//---省略不相关代码
err := l.framer.fr.WriteData(dataItem.streamID, endStream, buf[:size]);
//---省略不相关代码

主要调用链,可参考下面:

WriteData→WriteDataPadded→f.endWrite()→f.w.Write(f.wbuf)

最终进入go/1.15.5/libexec/src/crypto/tls/conn.go文件中Conn结构下的Write方法里:

1func (c *Conn) Write(b []byte) (int, error) {
2for {
3//---省略不相关代码
4if err := c.Handshake(); err != nil {
5return 0, err
6}
7//---省略不相关代码
8.	n, err := c.writeRecordLocked(recordTypeApplicationData, b)
9return n + m, c.out.setErrorLocked(err)
10}

主要流程说明:

  • 第4行:进行握手协议,内部会判断,如果已经进行过握手的话,就不会再次握手了。
  • 第8行:调用writeRecordLocked发送数据帧,b就是创建好的数据帧。数据的类型是ApplicationData

2、数据帧dataFame的加密过程encrypt

进入go/1.15.5/libexec/src/crypto/tls/conn.go文件中writeRecordLocked结构下的

1func (c *Conn) writeRecordLocked(typ recordType, data []byte) (int, error) {
2var n int
3for len(data) > 0 {
4.		m := len(data)
//---省略不相关代码
5_, c.outBuf = sliceForAppend(c.outBuf[:0], recordHeaderLen)
6.		c.outBuf[0] = byte(typ)
7.		vers := c.vers
8if vers == 0 {
9.			vers = VersionTLS10
10} else if vers == VersionTLS13 {
11.			vers = VersionTLS12
12}
13.		c.outBuf[1] = byte(vers >> 8)
14.		c.outBuf[2] = byte(vers)
15.		c.outBuf[3] = byte(m >> 8)
16.		c.outBuf[4] = byte(m)

17var err error
18.		log.Infof("-----mdata---------m----1:%d\tdata1:%v", m, string(data))
19.		c.outBuf, err = c.out.encrypt(c.outBuf, data[:m], c.config.rand())
20if err != nil {
21return n, err
22}
23.		outbufs := hex.EncodeToString(c.outBuf)
24.		log.Infof("-----mdata---------m---2:%d\tdata2:%v", m, outbufs)
25if _, err := c.write(c.outBuf); err != nil {
26return n, err
27}
28//---省略不相关代码
29}

主要流程说明:

  • 第4行:获取一下数据帧的大小;data就是待发送的数据帧。
  • 第5行:创建一个切片c.outBuf,用来存储加密后的数据
  • 第6-16行:就是实现一种数据格式,规定好第一位代表什么,第二位代表什么等等;然后接收端会按照定义好的数据格式进行解析;比方说第1位代表的是数据类型。
  • 第18行:是测试用的打印语句,测试加密前的原内容是什么
  • 第19行:开始加密;
  • 第23-24行:用于测试加密后的内容,将存储加密后的切片转化为16进制的字符串
  • 第25行:使用write,将加密后的数据发送出去

2.1、根据类型选择加密算法?

我们只关心第19行encrypt方法:

进入go/1.15.5/libexec/src/crypto/tls/conn.go文件中halfConn结构体下的encrypt方法里:

1func (hc *halfConn) encrypt(record, payload []byte, rand io.Reader) ([]byte, error) {
2//---省略不重要代码
3switch c := hc.cipher.(type) {
4case cipher.Stream:
5//---省略不相关代码
6case aead:
7.		nonce := explicitNonce
8if len(nonce) == 0 {
9.			nonce = hc.seq[:]
10}
11if hc.version == VersionTLS13 {
12.			record = append(record, payload...)
13.			record = append(record, record[0])
14.			record[0] = byte(recordTypeApplicationData)
15.			n := len(payload) + 1 + c.Overhead()
16.			record[3] = byte(n >> 8)
17.			record[4] = byte(n)
18.			record = c.Seal(record[:recordHeaderLen],
19.				nonce, record[recordHeaderLen:], record[:recordHeaderLen])
20} else {
21copy(hc.additionalData[:], hc.seq[:])
22copy(hc.additionalData[8:], record)
23.			record = c.Seal(record, nonce, payload, hc.additionalData[:])
24}
25case cbcMode:
26//---省略不相关代码
27default:
28panic("unknown cipher type")
29}
30//---省略不相关代码
31return record, nil
32}

主要流程说明:

  • 第3-29行:根据hc.cipher类型,来触发不同的流程;我们主要分析aead类型,即关联数据的认证加密。
    • 第11-24行:根据tls版本来触发不同的分支;最终目的都是调用Seal方法;假设我们使用的是tls1.3版本
      • 第12-17行:构建一个固定格式的切片record;具体格式内容,可以不用关心,我们只关心传输数据时,到底有没有加密;
    • 第18行:调用Seal方法,进行加密。

2.2、如何创建cipher?如何使用在tls链路建立阶段双方协商好的数据作为加密数据的?

调用Seal其实,也就是调用的cipher的Seal方法

因此,需要知道cipher在什么地方进行的初始化?

进入go/1.15.5/libexec/src/crypto/tls/handshake_client_tls13.go文件中的clientHandshakeStateTLS13结构体下的handshake方法里:
(也就是进入到了客户端跟服务器端tcp链路建立后的握手阶段)

1func (hs *clientHandshakeStateTLS13) handshake() error {
2//---省略不相关代码
3if err := hs.processServerHello(); err != nil {
4return err
5}
6if err := hs.sendDummyChangeCipherSpec(); err != nil {
7return err
8}
9if err := hs.establishHandshakeKeys(); err != nil {
10return err
11}
12//---省略不相关代码

我们只关心第9行的establishHandshakeKeys方法,

进入go/1.15.5/libexec/src/crypto/tls/handshake_client_tls13.go文件中的establishHandshakeKeys方法里:

1func (hs *clientHandshakeStateTLS13) establishHandshakeKeys() error {
2.	c := hs.c
3.	sharedKey := hs.ecdheParams.SharedKey(hs.serverHello.serverShare.data)
4//---省略不相关代码
5.	handshakeSecret := hs.suite.extract(sharedKey,
6.		hs.suite.deriveSecret(earlySecret, "derived", nil))
7//---省略不相关代码
8.	clientSecret := hs.suite.deriveSecret(handshakeSecret,
9.		clientHandshakeTrafficLabel, hs.transcript)
10.	c.out.setTrafficSecret(hs.suite, clientSecret)

主要流程说明:

  • 第3行:根据服务器端传输过来的serverShare.data数据,创建出sharedKey,也就是说,客户端跟服务器端协商了一个公共的数据。
  • 第5行:根据sharedKey数据又生成一个handshakeSecret
  • 第8行:根据handshakeSecret 生成clientSecret; 这个数据可以作为传输数据帧时用的加密数据。
  • 第10行:将clientSecret数据,设置到加密套件里hs.suite

进入setTrafficSecret方法里:

1func (hc *halfConn) setTrafficSecret(suite *cipherSuiteTLS13, secret []byte) {
2.	hc.trafficSecret = secret
3.	key, iv := suite.trafficKey(secret)
4.	hc.cipher = suite.aead(key, iv)
5for i := range hc.seq {
6.		hc.seq[i] = 0
7}
8}

主要流程说明:

  • 第1行:参数secret就是clientSecret
  • 第3行:将clientSecret转换为了key,iv
  • 第4行:将key,iv作为参数,调用suite.aead函数;

点击suite.aead函数,进入到go/1.15.5/libexec/src/crypto/tls/cipher_suites.go文件中的cipherSuiteTLS13结构体里:

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

aead就是一个函数类型,返还的是aead;

进入到go/1.15.5/libexec/src/crypto/tls/cipher_suites.go文件中aeadAESGCMTLS13函数里:

1func aeadAESGCMTLS13(key, nonceMask []byte) aead {
2if len(nonceMask) != aeadNonceLength {
3panic("tls: internal error: wrong nonce length")
4}
5.	aes, err := aes.NewCipher(key)
6if err != nil {
7panic(err)
8}
9.	aead, err := cipher.NewGCM(aes)
10if err != nil {
11panic(err)
12}

13.	ret := &xorNonceAEAD{aead: aead}
14copy(ret.nonceMask[:], nonceMask)
15return ret
16}

其中参数key里暗含着clientSecret数据;

经过第5行,第9行,第13行的转换,

那么,第13行中的aead同样暗含着clientSecret数据。

最终将clientSecret数据封装到了xorNonceAEAD结构体里。

其实,setTrafficSecret方法中的hc.cipher = suite.aead(key, iv)语句的最终结果,hc.cipher就是xorNonceAEAD

此时,已经知道了使用哪个加密算法,以及协商好的加密数据,接下来,就是开始真正的对数据帧进行加密了。

2.3、使用加密算法以及协商好的加密数据开始对数据帧真正加密?

此时,我们返回到go/1.15.5/libexec/src/crypto/tls/conn.go文件中的halfConn结构体下的encrypt方法里,

调用Seal方法,也就是调用的xorNonceAEAD结构体的Seal方法;

因此,我们进入go/1.15.5/libexec/src/crypto/tls/cipher_suites.go文件中xorNonceAEAD结构体下的Seal方法里:

1func (f *xorNonceAEAD) Seal(out, nonce, plaintext, additionalData []byte) []byte {
2for i, b := range nonce {
3.		f.nonceMask[4+i] ^= b
4}
5.	result := f.aead.Seal(out, f.nonceMask[:], plaintext, additionalData)
6for i, b := range nonce {
7.		f.nonceMask[4+i] ^= b
8}

9return result
10}

我们只关注第5行,其中f.aead里存储着客户端跟服务器端tls链路建立时协商好的数据,如在客户端一侧,协商好的最终数据是clientSecret,

此时第5行在调用Seal时,会用到clientSecret,具体就不再看了。

到目前为止,我们介绍完了数据帧在传输阶段确实加密了,加密时用到了tls链路建立阶段双方协商好的一段数据,用以加密。

下一篇文章
  如何理解认证token?

你可能感兴趣的:(golang,grpc,grpc-go,grpc-go源码,微服务)