20

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

 4 years ago
source link: https://studygolang.com/articles/25201
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

Licensed under CC BY-SA 4.0


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK