寫給 go 開發者的 gRPC 教程 - 安全
使用 TLS 安全傳輸數據
什麼是 SSL/TLS
SSL 包含記錄層(Record Layer)和傳輸層 [1],記錄層協議確定傳輸層數據的封裝格式。傳輸層安全協議使用 X.509[2] 認證,之後利用非對稱加密演算來對通信方做身份認證,之後交換對稱密匙作爲會話密匙(Session key[3])。這個會談密匙是用來將通信兩方交換的資料做加密,保證兩個應用間通信的保密性和可靠性,使客戶與服務器應用之間的通信不被攻擊者竊聽。
--- 維基百科
簡單點說就是:SSL/TLS 是一個安全協議,它通過一系列的手段、一系列的算法讓客戶端與服務端之間加密傳輸數據,避免數據被攻擊者竊聽。快速入門 TLS 可以參考:一文帶你快速入門 TLS/SSL
SSL/TLS 分爲單向認證和雙向認證(mtls)
單向認證
在單向認證中,僅客戶端驗證服務端
當客戶端和服務端建立連接之後,服務端會發送公開的證書給客戶端,客戶端驗證證書後使用證書中包含的密鑰信息來發送加密數據(實際要比這個複雜,這裏簡化了交互流程)
TLS 證書可以使用根證書創建子證書,因此對於證書有兩種使用方式,一種是直接使用根證書,一種是使用由根證書籤發的子證書
✨ 直接使用根證書
所以對於服務端來說需要兩個文件
server.key
:RSA 的私鑰,用來進行數字簽名
server.pem/server.crt
:自簽名的服務端證書,其中包含與私鑰對應的公鑰、網站域名、簽名算法等信息
對於客戶端來說不需要準備文件
服務端代碼如下:
1)NewServerTLSFromFile
加載證書 2)NewServer
時指定 Creds
func main() {
l, err := net.Listen("tcp", ":8009")
if err != nil {
panic(err)
}
// method 1.
creds, err := credentials.NewServerTLSFromFile("./x509/server.crt", "./x509/server.key")
if err != nil {
panic(err)
}
// method 2.
// cert , err := tls.LoadX509KeyPair("./x509/server.crt", "./x509/server.key")
// if err != nil {
// panic(err)
// }
// creds := credentials.NewServerTLSFromCert(&cert)
s := grpc.NewServer(grpc.Creds(creds))
pb.RegisterOrderManagementServer(s, &server{})
if err := s.Serve(l); err != nil {
panic(err)
}
}
客戶端代碼如下:
1)NewClientTLSFromFile
指定使用 CA 證書來校驗服務端的證書有效性。
- 注意:第二個參數域名就是服務端證書時的 CN 參數
2)建立連接時 指定建立安全連接WithTransportCredentials
func main() {
creds, err := credentials.NewClientTLSFromFile("./x509/server.crt", "www.example.com")
if err != nil {
panic(err)
}
conn, err := grpc.Dial("localhost:8009", grpc.WithTransportCredentials(creds))
if err != nil {
panic(err)
}
defer conn.Close()
client := pb.NewOrderManagementClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
// Get Order
retrievedOrder, err := client.GetOrder(ctx, &wrapperspb.StringValue{Value: "101"})
if err != nil {
panic(err)
}
log.Print("GetOrder Response -> : ", retrievedOrder)
}
✨ 根證書模式
在生產環境通常是生成一個 CA 根證書,然後使用 CA 根證書去簽名多個服務端的證書。這種方式可以一次管理多個證書,也比較貼近真實情況,當然也可以用來做證書過期、更新等試驗,缺點是操作起來略微麻煩一點。
rootCA.key
:ca 機構的私鑰,用來給服務端簽發證書
rootCA.crt
:ca 的證書,用來給客戶端驗證服務端證書
server.key
:RSA 的私鑰,用來進行數字簽名
server.pem/server.crt
:由 ca 簽發的服務端證書
服務端代碼如下:
1)NewServerTLSFromFile
加載證書 2)NewServer
時指定 Creds
func main() {
l, err := net.Listen("tcp", ":8009")
if err != nil {
panic(err)
}
// method 1.
creds, err := credentials.NewServerTLSFromFile("./x509/server.crt", "./x509/server.key")
if err != nil {
panic(err)
}
// method 2.
// cert , err := tls.LoadX509KeyPair("./x509/server.crt", "./x509/server.key")
// if err != nil {
// panic(err)
// }
// creds := credentials.NewServerTLSFromCert(&cert)
s := grpc.NewServer(grpc.Creds(creds))
pb.RegisterOrderManagementServer(s, &server{})
if err := s.Serve(l); err != nil {
panic(err)
}
}
客戶端代碼如下:
1)NewClientTLSFromFile
指定使用 CA 根證書來校驗服務端的證書有效性。
- 注意:第二個參數域名就是服務端證書時的 CN 參數
2)建立連接時 指定建立安全連接WithTransportCredentials
func main() {
creds, err := credentials.NewClientTLSFromFile("./x509/rootCa.crt", "www.example.com")
if err != nil {
panic(err)
}
conn, err := grpc.Dial("localhost:8009", grpc.WithTransportCredentials(creds))
if err != nil {
panic(err)
}
defer conn.Close()
client := pb.NewOrderManagementClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
// Get Order
retrievedOrder, err := client.GetOrder(ctx, &wrapperspb.StringValue{Value: "101"})
if err != nil {
panic(err)
}
log.Print("GetOrder Response -> : ", retrievedOrder)
}
雙向認證(mTLS)
server-side TLS 中雖然服務端使用了證書,但是客戶端卻沒有使用證書,本章節會給客戶端也生成一個證書,並完成 mutual TLS
直接使用根證書
在 mTLS 中很少會有直接使用根證書的場景,這裏僅放一個交互圖,不放代碼了
根證書模式
rootCA.key
:ca 機構的私鑰,用來給服務端簽發證書
rootCA.crt
:ca 的證書,用來給客戶端驗證服務端證書
server.key
:服務端 RSA 的私鑰,用來進行數字簽名
server.pem/server.crt
:由 ca 簽發的服務端證書
client.key
: 客戶端 RSA 私鑰,用來進行數字簽名
client.pem/client.crt
: 由 ca 簽發的客戶端證書
服務端代碼如下:
1)加載服務端證書
2)構建用於校驗客戶端證書的CertPool
3)使用上面的參數構建一個TransportCredentials
4)newServer
是指定使用前面創建的creds
看似改動很大,其實如果你仔細查看了前面
NewServerTLSFromFile
方法做的事的話,就會發現是差不多的,只有極個別參數不同。修改點如下:
1)
tls.Config
的參數ClientAuth
,這裏改成了tls.RequireAndVerifyClientCert
,即服務端也必須校驗客戶端的證書,之前使用的默認值 (即不校驗)2)
tls.Config
的參數ClientCAs
,由於之前都不校驗客戶端證書,所以也沒有指定用什麼證書來校驗
func main() {
l, err := net.Listen("tcp", ":8009")
if err != nil {
panic(err)
}
certificate, err := tls.LoadX509KeyPair("./x509/server.crt", "./x509/server.key")
if err != nil {
panic(err)
}
// 創建CertPool,後續就用池裏的證書來校驗客戶端證書有效性
// 所以如果有多個客戶端 可以給每個客戶端使用不同的 CA 證書,來實現分別校驗的目的
certPool := x509.NewCertPool()
ca, err := ioutil.ReadFile("./x509/rootCa.crt")
if err != nil {
panic(err)
}
if ok := certPool.AppendCertsFromPEM(ca); !ok {
log.Fatal("failed to append certs")
}
// 構建基於 TLS 的 TransportCredentials
creds := credentials.NewTLS(&tls.Config{
// 設置證書鏈,允許包含一個或多個
Certificates: []tls.Certificate{certificate},
// 要求必須校驗客戶端的證書 可以根據實際情況選用其他參數
ClientAuth: tls.RequireAndVerifyClientCert, // NOTE: this is optional!
// 設置根證書的集合,校驗方式使用 ClientAuth 中設定的模式
ClientCAs: certPool,
})
s := grpc.NewServer(grpc.Creds(creds))
pb.RegisterOrderManagementServer(s, &server{})
if err := s.Serve(l); err != nil {
panic(err)
}
}
客戶端代碼如下:
客戶端改動和前面服務端差不多,具體步驟都一樣,除了不能指定校驗策略之外基本一樣。
func main() {
// 加載客戶端證書
certificate, err := tls.LoadX509KeyPair("x509/client.crt", "x509/client.key")
if err != nil {
log.Fatal(err)
}
// 構建CertPool以校驗服務端證書有效性
b, err := ioutil.ReadFile("./x509/rootCa.crt")
if err != nil {
log.Fatal(err)
}
cp := x509.NewCertPool()
if !cp.AppendCertsFromPEM(b) {
log.Fatal("credentials: failed to append certificates")
}
creds := credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{certificate},
ServerName: "www.example.com",
RootCAs: cp,
})
conn, err := grpc.Dial("localhost:8009", grpc.WithTransportCredentials(creds))
if err != nil {
panic(err)
}
defer conn.Close()
client := pb.NewOrderManagementClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
// Get Order
retrievedOrder, err := client.GetOrder(ctx, &wrapperspb.StringValue{Value: "101"})
if err != nil {
panic(err)
}
log.Print("GetOrder Response -> : ", retrievedOrder)
}
可能遇到的問題
報錯:transport: authentication handshake failed: x509: certificate relies on legacy Common Name field, use SANs instead
如果出現上述報錯,是因爲 go 1.15 版本開始廢棄 CommonName[4],因此推薦使用 SAN 證書。如果想兼容之前的方式,需要設置環境變量 GODEBUG 爲 GODEBUG=x509ignoreCN=0
。
**什麼是 SAN?**SAN(Subject Alternative Name) 是 SSL 標準 x509 中定義的一個擴展。使用了 SAN 字段的 SSL 證書,可以擴展此證書支持的域名,使得一個證書可以支持多個不同域名的解析。
SAN 證書包含 Subject Alternative Name 部分
openssl x509 -text -noout -in server.crt | grep -A 1 "Subject Alternative Name"
X509v3 Subject Alternative Name:
DNS:www.example.com, DNS:localhost, DNS:127.0.0.1, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1
證書生成
📖 方式一:生成自簽名的證書
openssl req -config conf/openssl.cnf -x509 -newkey rsa:2048 -nodes -days 365 \
-keyout server.key \
-out server.crt
📖 方式二:根證書籤發子證書
1、創建自簽名的 ca 根證書
openssl req -config conf/openssl.cnf -x509 -newkey rsa:2048 -nodes -days 365 \
-keyout rootCa.key \
-out rootCa.crt
2、創建服務端私鑰和 CSR(證書籤名請求),這裏沒有指定 - x509, 說明這不是一個自簽署的證書,而是要交給 CA 簽名認證
openssl req -config conf/openssl.cnf -newkey rsa:2048 -nodes \
-keyout server.key \
-out server.csr
通過下列命令檢查一下生成的 csr 文件是否信息正確無誤。
openssl req -text -noout -verify -in server.csr
3、使用證書機構的私鑰、根證書和 example.com 的 CSR 創建數字證書
⚠️ 不加 - extfile 和 - extensions 參數是不行的
openssl x509 -req -in server.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial \
-extfile conf/openssl.cnf \
-extensions v3_req \
-out server.crt
簽署後,查看證書的內容:
openssl x509 -text -noout -in server.crt
參考資料
[1]
傳輸層: https://zh.wikipedia.org/wiki / 傳輸層
[2]
X.509: https://zh.wikipedia.org/wiki/X.509
[3]
Session key: https://zh.wikipedia.org/wiki/Session_key
[4]
廢棄 CommonName: https://golang.org/doc/go1.15#commonname
[5]
使用 OpenSSL 生成含有 Subject Alternative Name(SAN) 的證書: https://blog.ideawand.com/2017/11/22/build-certificate-that-support-Subject-Alternative-Name-SAN/
[6]
Steps to generate CSR for SAN certificate with openssl: https://www.golinuxcloud.com/openssl-subject-alternative-name/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Dt_0iL5bGK5Nj_scI1X-_g