寫給 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 證書來校驗服務端的證書有效性。

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 根證書來校驗服務端的證書有效性。

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