gRPC實踐--加密通信
前提閱讀:
本文將在上一篇的代碼基礎(chǔ)上,進(jìn)行加密通信的編寫。
在gRPC的demo通信中,有服務(wù)端和客戶端,他們之間都是明文通信的,這樣會帶來安全隱患。
一般的,gRPC有幾種方法對server 和 client 之間的通信進(jìn)行加密,即,身份驗證機(jī)制:

這里,介紹最常用的證書認(rèn)證機(jī)制,通過使用證書認(rèn)證,在TLS協(xié)議下,實現(xiàn)加密通信。
我們知道,gRPC建立在HTTP/2協(xié)議之上,所以對 TLS 提供了很好的支持。
之前的gRPC的服務(wù)都沒有提供證書支持,因此客戶端在鏈接服務(wù)器中通過
grpc.WithInsecure()選項跳過了對服務(wù)器證書的驗證。沒有啟用證書的gRPC服務(wù)在和客戶端進(jìn)行的是明文通訊,信息面臨被任何第三方監(jiān)聽的風(fēng)險。為了保障gRPC通信不被第三方監(jiān)聽篡改或偽造,我們可以對服務(wù)器啟動TLS加密特性。---------《Go語言高級編程(Advanced Go Programming)》
證書
在go 版本在1.15以下時,參考博客:帶入gRPC:TLS 證書認(rèn)證,我們可以輕松完成證書的加密通信。但是,在go v1.15以后,如果使用一般的證書通信,則會報錯:
2021/04/24 15:37:11 could not greet: rpc error: code = Unavailable desc = connection error: desc = "transport: authentication handshake failed: x509: certificate relies on legacy Common Name field, use SANs or temporarily enable Common Name matching with GODEBUG=x509ignoreCN=0"
總的來說,就是:
身份驗證失敗了,因為GO 1.15以后X509 不能用了,提示我們有兩個選擇:
1. 需要使用SAN 證書
2. 改變環(huán)境變量:GODEBUG=x509ignoreCN=0
這里,我們選擇使用SAN證書,進(jìn)行通信
SAN and openssl
首先,我們看看什么是SAN證書:
SAN(Subject Alternative Name) 是 SSL 標(biāo)準(zhǔn) x509 中定義的一個擴(kuò)展。使用了 SAN 字段的 SSL 證書,可以擴(kuò)展此證書支持的域名,使得一個證書可以支持多個不同域名的解析。
要生成SAN證書,需要以下幾步:
1. 修改openssl.cfg
如果是Linux系統(tǒng)的話,應(yīng)該修改的是openssl.cnf文件,我這個是windos,所以就是openssl.cfg。
找到你安裝openssl 程序的目錄,我的為E:\OpenSSL-Win64\bin,如果沒有該程序,可以從這里官網(wǎng)下載。
將openssl.cfg文件拷貝到需要生成證書的那個目錄,進(jìn)行修改
- 打開copy_extensions 在CA_default節(jié)
[ CA_default ]
...
copy_extensions = copy # Extension copying option: use with caution.
...
- 打開req_extensions 在req中修改。
[ req ]
...
req_extensions = v3_req # The extensions to add to a certificate request
...
這段配置表示在生成 CSR 文件時讀取名叫
v3_req的段落的配置信息,因此我們再在此配置文件中加入一段名為v3_req的配置:
- 增加subjectAltName 在v3_req節(jié)
[ v3_req ]
...
subjectAltName = @alt_names
## 這段配置中最重要的是在最后導(dǎo)入名為 alt_names 的配置段,
## 因此我們還需要添加一個名為 [ alt_names ] 的配置段,這可以定義多個服務(wù)
[alt_names]
DNS.1 = *.org.example.com
DNS.2 = *.example.com
參考:[使用openssl創(chuàng)建包含SAN的證書](使用openssl創(chuàng)建包含SAN的證書 - 簡書 (jianshu.com))
2. 生成根證書
現(xiàn)在,開始生成證書,首先是生成CA證書:
生成CA私鑰(.key)–>生成CA證書請求(.csr)–>自簽名得到根證書(.crt)(CA給自已頒發(fā)的證書)。
生成根證書私鑰
openssl genrsa -out ca.key 2048
生成CA證書請求
openssl req -new -key ca.key -out ca.csr -subj "/C=cn/OU=myorg/O=mytest/CN=myname"
自簽名得到根證書
openssl x509 -req -days 3650 -in ca.csr -signkey ca.key -out ca.crt
其中:
- genrsa:使用RSA算法產(chǎn)生私鑰
- -in:要輸入的csr文件
- -out:輸出文件的路徑
- -subj:證書相關(guān)的用戶信息(subject的縮寫)
- -key:指定私鑰路徑
- -new:新證書簽發(fā)請求
- -req:輸入csr文件
- -days:證書的有效期(天)
3. 生成SNA的服務(wù)端證書
生成服務(wù)端私鑰(serve.key)–>生成服務(wù)端證書請求(server.csr)–>CA對服務(wù)端請求文件簽名,生成服務(wù)端證書(server.pem)
生成服務(wù)端證書私鑰
openssl genrsa -out server.key 2048
根據(jù)私鑰server.key生成證書請求文件server.csr
$ openssl req -new -nodes -key server.key -out server.csr -subj "/C=cn/OU=myserver/O=servercomp/CN=servername" -config ./openssl.cfg -extensions v3_req
請求CA對證書請求文件簽名,生成最終證書文件
λ openssl x509 -req -days 365 -in server.csr -out server.pem -CA ca.crt -CAkey ca.key -CAcreateserial -extfile ./openssl.cfg -extensions v3_req
Signature ok
subject=C = cn, OU = myserver, O = servercomp, CN = servername
Getting CA Private Key
驗證:
λ openssl x509 -noout -text -in server.pem
Certificate:
Data:
......
X509v3 extensions:
X509v3 Basic Constraints:
CA:FALSE
X509v3 Key Usage:
Digital Signature, Non Repudiation, Key Encipherment
X509v3 Subject Alternative Name:
DNS:*.org.haha.com, DNS:*.haha.com
......
代碼實現(xiàn)
在實現(xiàn)代碼之前,我們注意到client.go 中的一個代碼:
// 連接 gRPC 服務(wù)器
conn, err := grpc.Dial(address, grpc.WithInsecure())
其中,grpc.WithInsecure() 函數(shù),正如開頭引用中提到的,這個選項會使得程序跳過對服務(wù)器證書的驗證,這樣服務(wù)端和客戶端就成立明文通信。
事實上,WithInsecure()最終會通過讀取設(shè)置的值來禁用安全傳輸,具體說明,可以參考下面這篇博客:帶入gRPC:TLS 證書認(rèn)證 - SegmentFault 思否
有了證書之后,我們就可以在啟動gRPC服務(wù)時傳入證書選項參數(shù):

在server 和 client 上實現(xiàn) TLS 證書認(rèn)證的加密通信:
目錄結(jié)構(gòu)
λ tree /F
D:.
│ go.mod
│ go.sum
├─cert
│ ca.crt
│ ca.csr
│ ca.key
│ openssl.cfg
│ server.csr
│ server.key
│ server.pem
├─client
│ client.go
├─proto
│ helloworld.pb.go
│ helloworld.proto
└─server
server.go
Server

其中:
-
credentials.NewServerTLSFromFile:根據(jù)服務(wù)端輸入的證書文件和密鑰構(gòu)造 TLS 憑證
func NewServerTLSFromFile(certFile, keyFile string) (TransportCredentials, error) {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return nil, err
}
return NewTLS(&tls.Config{Certificates: []tls.Certificate{cert}}), nil
}
-
grpc.Creds():返回一個 ServerOption,用于設(shè)置服務(wù)器連接的憑據(jù)。用于grpc.NewServer(opt ...ServerOption)為 gRPC Server 設(shè)置連接選項
func Creds(c credentials.TransportCredentials) ServerOption {
return func(o *options) {
o.creds = c
}
}
經(jīng)過以上兩個簡單步驟,gRPC Server 就建立起需證書認(rèn)證的服務(wù)啦。
Client

其中:
-
credentials.NewClientTLSFromFile():根據(jù)客戶端輸入的證書文件和密鑰構(gòu)造 TLS 憑證。serverNameOverride 為服務(wù)名稱
func NewClientTLSFromFile(certFile, serverNameOverride string) (TransportCredentials, error) {
b, err := ioutil.ReadFile(certFile)
if err != nil {
return nil, err
}
cp := x509.NewCertPool()
if !cp.AppendCertsFromPEM(b) {
return nil, fmt.Errorf("credentials: failed to append certificates")
}
return NewTLS(&tls.Config{ServerName: serverNameOverride, RootCAs: cp}), nil
}
-
grpc.WithTransportCredentials():返回一個配置連接的 DialOption 選項。用于grpc.Dial(target string, opts ...DialOption)設(shè)置連接選項
func WithTransportCredentials(creds credentials.TransportCredentials) DialOption {
return newFuncDialOption(func(o *dialOptions) {
o.copts.TransportCredentials = creds
})
}
驗證

總結(jié)
本文講述了gRPC的證書通信的簡單示例。主要為服務(wù)端頒發(fā)了證書,之后服務(wù)端和客戶端都基于一個證書進(jìn)行TLS的加密通信。
但,在實際生產(chǎn)環(huán)境中,以上這種方式,需要提前將服務(wù)器的證書告知客戶端,這樣客戶端在鏈接服務(wù)器時才能進(jìn)行對服務(wù)器證書認(rèn)證。在復(fù)雜的網(wǎng)絡(luò)環(huán)境中,服務(wù)器證書的傳輸本身也是一個非常危險的問題。如果在中間某個環(huán)節(jié),服務(wù)器證書被監(jiān)聽或替換那么對服務(wù)器的認(rèn)證也將不再可靠。
為了避免證書的傳遞過程中被篡改,可以通過一個安全可靠的根證書分別對服務(wù)器和客戶端的證書進(jìn)行簽名。這樣客戶端或服務(wù)器在收到對方的證書后可以通過根證書進(jìn)行驗證證書的有效性。