本篇主要进行加密非流式 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
有疑问加站长微信联系(非本文作者)