深入学习 GRPC – 2. 加密非流式的字节结构
本篇主要进行加密非流式 GRPC 的通信在字节层面的讨论,使用带 TLSv1.2 的 nginx 节点代理非加密的 golang 服务端节点,密钥交换使用椭圆曲线,在服务端使用自签名证书,不使用客户端证书,假设读者对 TLS 等已有基本的了解。
使用以下命令生成椭圆曲线密钥和服务端自签名证书:
openssl ecparam -genkey -name secp256r1 | openssl ec -out hot.key -aes128 openssl req -new -x509 -days 365 -key hot.key -out hot.crt
上一篇的 proto 和 golang 服务端代码不变,golang 客户端代码变为:
package main import ( "context" "crypto/tls" "os" "grpc_hot/pb" "google.golang.org/grpc" "google.golang.org/grpc/credentials" ) func main() { port := "30080" if len(os.Args) >= 2 { port = os.Args[1] } creds := credentials.NewTLS(&tls.Config{ InsecureSkipVerify: true, }) conn, err := grpc.Dial("127.0.0.1:"+port, grpc.WithTransportCredentials(creds)) if nil != err { println(err.Error()) return } defer conn.Close() cli := pb.NewHotClient(conn) resp, err := cli.Inc(context.Background(), &pb.IntReq{I: 6}) if nil != err { println(err.Error()) return } println("resp:", resp.GetI()) }
nginx 配置文件变为:
upstream grpc_hot { server 127.0.0.1:30081; server 127.0.0.1:30082; } server { listen 30080 ssl http2; ssl_protocols TLSv1.2; ssl_certificate hot.crt; ssl_certificate_key hot.key; ssl_password_file hot.pass; ssl_prefer_server_ciphers on; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256; ssl_session_cache shared:grpc_hot_sess:32m; ssl_session_timeout 10m; keepalive_timeout 60; location / { grpc_pass grpc://grpc_hot; } }
2.1. TLS
启动上述 golang 的服务端和 nginx,调用一次客户端,在客户端连接 30080
端口。使用 wireshark 抓包,总共抓到 40 帧,基本上比第 1 篇多了一倍。
在 OSI 七层结构中,TCP、TLS、HTTP 分别位居第 4、6、7 层,这里第 5 层为空。本篇中我们当然只关心 TCP 的荷载为 TLS 层的帧。TLS 层的结构如下:
+---------------+-------------------------------+------------------------------+ | Cont Type (8) | Version (16) | Length (16) | +---------------+-------------------------------+------------------------------+ | Data (*) ... +------------------------------------------------------------------------------+
在第 4、6、8、9 帧,两端完成了 10 步的 TLS 握手:
-
Client Hello
/Server Hello
:两端各生成一个 随机串
告知对方,并由服务端决定使用套件ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
-
Certificate
:服务端下发证书,包括公钥。客户端验证证书,这里选择不验证 -
Server Key Exchange
/Server Hello Done
:服务端随机生成一个 服务端临时私钥
,根据该私钥在椭圆曲线上计算出一个 服务端临时公钥
,下发给客户端 -
Client Key Exchange
/Client Change Cipher Spec
/Client Finished
:同样,客户端随机生成一个 客户端临时私钥
,根据该私钥在椭圆曲线上计算出一个 客户端临时公钥
,上传给服务端。同时,客户端根据 hello 步的两个随机串、客户端临时私钥和服务端临时公钥,计算出两端分别使用的 对称密钥 -
Server Change Cipher Spec
/Server Finished
:同样,服务端根据 hello 步的两个随机串、服务端临时私钥和客户端临时公钥,计算出两端分别使用的对称密钥。数学的魔力保证了两端分别计算出的对称密钥必然相同,感觉这很浪漫啊。
2.2 HTTP/2
接下来抓到 9 个 TLS 层的帧,它们的 Content type
均为 Application Data (23)
,显然,其中的 Data
字段均为已被对称密钥加密的内容,解密之后即是 HTTP 层的内容。
这里我们修改了 golang 标准库的 crypto/tls.(*halfConn).encrypt
和 crypto/tls.(*halfConn).decrypt
函数,分别在其中打印出加密前和解密后的数据。我们还是逐帧看看它们:
frame | source | TLS payload(decrypted) / HTTP content |
---|---|---|
10 | server |
00 00 12 04 00 00 00 00 00 00 03 00 00 00 80 00 04 00 01 00 00 00 05 00 FF FF FF 00 00 04 08 00 00 00 00 00 7F FF 00 00
|
11 | client |
50 52 49 20 2A 20 48 54 54 50 2F 32 2E 30 0D 0A 0D 0A 53 4D 0D 0A 0D 0A
|
12 | client |
00 00 00 04 00 00 00 00 00
|
14 | server |
00 00 00 04 01 00 00 00 00
|
15 | client |
00 00 00 04 01 00 00 00 00
|
16 | client |
00 00 3E 01 04 00 00 00 01 83 87 45 89 62 B8 D7 C6 74 B1 92 A2 7F 41 8B 08 9D 5C 0B 81 70 DC 64 00 78 1F 5F 8B 1D 75 D0 62 0D 26 3D 4C 4D 65 64 7A 8A 9A CA C8 B4 C7 60 2B 89 B5 C3 40 02 74 65 86 4D 83 35 05 B1 1F 00 00 07 00 01 00 00 00 01 00 00 00 00 02 08 06
|
33 | server |
00 00 35 01 04 00 00 00 01 88 76 8D 3D 65 AA C2 A1 3E 98 0A E1 6D 77 97 17 61 96 DC 34 FD 28 07 54 BE 52 28 20 05 F5 00 ED C6 9B B8 07 54 C5 A3 7F 5F 8B 1D 75 D0 62 0D 26 3D 4C 4D 65 64 00 00 07 00 00 00 00 00 01 00 00 00 00 02 08 07
|
35 | server |
00 00 18 01 05 00 00 00 01 00 88 9A CA C8 B2 12 34 DA 8F 01 30 00 89 9A CA C8 B5 25 42 07 31 7F 00
|
39 | client |
00 00 04 08 00 00 00 00 00 00 00 00 07 00 00 08 06 00 00 00 00 00 02 04 10 10 09 0E 07 07
|
拨云见日,熟悉的亚子又回来了。可以看到,服务端的 SETTINGS
帧早于客户端的试探帧,其他差不都不大。
其中,第 16、33、35 帧的首部解码出来分别如下:
:method POST :scheme https :path /pb.Hot/Inc :authority 127.0.0.1:30080 content-type application/grpc user-agent grpc-go/1.25.1 te trailers
:status 200 server openresty/1.15.8.2 date Sat, 07 Dec 2019 07:45:07 GMT content-type application/grpc
grpc-status 0 grpc-message
请求首部的 :scheme
字段变为了 https
,其它都没有什么变化。而两个 DATA
帧也还是我们熟悉的样子。
References
Elliptic Curve Cryptography: a gentle introduction
RFC-5246: The Transport Layer Security (TLS) Protocol Version 1.2
Licensed under CC BY-SA 4.0