深入学习 GRPC - 2. 加密非流式的字节结构

Platanuses · · 1741 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

本篇主要进行加密非流式 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).encryptcrypto/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


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:Platanuses

查看原文:深入学习 GRPC - 2. 加密非流式的字节结构

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1741 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传