RPC 編程 -六-:gRPC 中的證書認證

  1. 介紹

gRPC建立在HTTP/2協議之上,對TLS提供了很好的支持。當不需要證書認證時, 可通過grpc.WithInsecure()選項跳過了對服務器證書的驗證,沒有啓用證書的gRPC服務和客戶端進行的是明文通信,信息面臨被任何第三方監聽的風險。爲了保證gRPC通信不被第三方監聽、篡改或僞造,可以對服務器啓動TLS加密特性。

  1. 概念簡述

2.1 什麼是 CA

CACertificate Authority的縮寫,也叫 “證書授權中心”。它是負責管理和簽發證書的第三方機構,作用是檢查證書持有者身份的合法性,並簽發證書,以防證書被僞造或篡改。

CA實際上是一個機構,負責 “證件” 印製核發。就像負責頒發身份證的公安局、負責發放行駛證、駕駛證的車管所。

2.2 什麼是 CA 證書

CA 證書就是CA頒發的證書。我們常聽到的數字證書就是CA證書,CA證書包含信息有: 證書擁有者的身份信息,CA 機構的簽名,公鑰和私鑰,

2.3 什麼是SAN

SAN(Subject Alternative Name)SSL 標準 x509 中定義的一個擴展。使用了 SAN 字段的 SSL 證書,可以擴展此證書支持的域名,使得一個證書可以支持多個不同域名的解析。

2.4 爲什麼需要SAN

先看下不通過SAN生成的證書,會報的錯誤信息。

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 版本開始廢棄 CommonName,因此推薦使用 SAN 證書。

  1. 證書生成流程

3.1 生成 CA 證書

1. 新增ca.conf

[ req ]
default_bits       = 4096
distinguished_name = req_distinguished_name

[ req_distinguished_name ]
countryName                 = GB
countryName_default         = BeiJing
stateOrProvinceName         = State or Province Name (full name)
stateOrProvinceName_default = JiangSu
localityName                = Locality Name (eg, city)
localityName_default        = NanJing
organizationName            = Organization Name (eg, company)
organizationName_default    = Step
commonName                  = liuqh.icu
commonName_max              = 64
commonName_default          = liuqh.icu

2. 生成 CA 私鑰

# 生成CA私鑰
$ openssl genrsa -out ca.key 4096

3. 生成 CA 證書

$ openssl req -new -x509 -days 365 -subj "/C=GB/L=Beijing/O=github/CN=liuqh.icu" \
-key ca.key -out ca.crt -config ca.conf

更多參數含義: https://www.digicert.com/kb/ssl-support/openssl-quick-reference-guide.htm#CreatingYourCSR

3.2 生成服務端證書

1. 新增server.conf

[ req ]
default_bits       = 2048
distinguished_name = req_distinguished_name

[ req_distinguished_name ]
countryName                 = Country Name (2 letter code)
countryName_default         = CN
stateOrProvinceName         = State or Province Name (full name)
stateOrProvinceName_default = JiangSu
localityName                = Locality Name (eg, city)
localityName_default        = NanJing
organizationName            = Organization Name (eg, company)
organizationName_default    = Step
commonName                  = CommonName (e.g. server FQDN or YOUR name)
commonName_max              = 64
commonName_default          = XXX(自定義,客戶端需要此字段做匹配)
[ req_ext ]
subjectAltName = @alt_names
[alt_names]
DNS.1   = liuqh.icu
IP      = 127.0.0.1

2. 生成公私鑰

$ openssl genrsa -out server.key 2048

3. 生成CSR

$ openssl req -new  -subj "/C=GB/L=Beijing/O=github/CN=liuqh.icu" \
-key server.key -out server.csr -config server.conf

4. 基於 CA 簽發證書

$ openssl x509 -req -sha256 -CA ca.crt -CAkey ca.key -CAcreateserial -days 365 \
-in server.csr -out server.crt -extensions req_ext -extfile server.conf

3.4 生成客戶端證書

1. 生成公私鑰

$ openssl genrsa -out client.key 2048

2. 生成CSR

$ openssl req -new -subj "/C=GB/L=Beijing/O=github/CN=liuqh.icu"  \
-key client.key -out client.csr

3. 基於 CA 簽發證書

$ openssl x509 -req -sha256 -CA ca.crt -CAkey ca.key -CAcreateserial -days 365 \
-in client.csr -out client.crt
  1. 目錄結構

  1. 代碼實現

5.1 服務端

package main

import (
 "52lu/go-rpc/server/tslservice"
 "crypto/tls"
 "crypto/x509"
 "fmt"
 "google.golang.org/grpc"
 "google.golang.org/grpc/credentials"
 "io/ioutil"
 "net"
)

func main() {
 // 公鑰中讀取和解析公鑰/私鑰對
 pair, err := tls.LoadX509KeyPair("./pem/server.crt""./pem/server.key")
 if err != nil {
  fmt.Println("LoadX509KeyPair error", err)
  return
 }
 // 創建一組根證書
 certPool := x509.NewCertPool()
 ca, err := ioutil.ReadFile("./pem/ca.crt")
 if err != nil {
  fmt.Println("read ca pem error ", err)
  return
 }
 // 解析證書
 if ok := certPool.AppendCertsFromPEM(ca); !ok {
  fmt.Println("AppendCertsFromPEM error ")
  return
 }
 cred := credentials.NewTLS(&tls.Config{
  Certificates: []tls.Certificate{pair},
  ClientAuth:   tls.RequireAndVerifyClientCert,
  ClientCAs:    certPool,
 })
 grpcServer := grpc.NewServer(grpc.Creds(cred))
 // 註冊服務
 tslservice.RegisterTslServiceServer(grpcServer, new(tslservice.UnimplementedTslServiceServer))
 // 監聽端口
 listen, err := net.Listen("tcp"":1234")
 if err != nil {
  fmt.Println(err)
  return
 }
 fmt.Println("服務啓動成功....")
 // 啓動服務
 grpcServer.Serve(listen)
}

5.2 客戶端

package main

import (
 "52lu/go-rpc/server/tslservice"
 "context"
 "crypto/tls"
 "crypto/x509"
 "fmt"
 "google.golang.org/grpc"
 "google.golang.org/grpc/credentials"
 wrapperspb "google.golang.org/protobuf/types/known/wrapperspb"
 "io/ioutil"
)

func main() {
 // 公鑰中讀取和解析公鑰/私鑰對
 pair, err := tls.LoadX509KeyPair("./pem/client.crt""./pem/client.key")
 if err != nil {
  fmt.Println("LoadX509KeyPair error ", err)
  return
 }
 // 創建一組根證書
 certPool := x509.NewCertPool()
 ca, err := ioutil.ReadFile("./pem/ca.crt")
 if err != nil {
  fmt.Println("ReadFile ca.crt error ", err)
  return
 }
 // 解析證書
 if ok := certPool.AppendCertsFromPEM(ca); !ok {
  fmt.Println("certPool.AppendCertsFromPEM error ")
  return
 }
 cred := credentials.NewTLS(&tls.Config{
  Certificates: []tls.Certificate{pair},
  //ServerName:   "liuqh.icu",
  ServerName: "test.com",
  RootCAs:    certPool,
 })
 conn, err := grpc.Dial(":1234", grpc.WithTransportCredentials(cred))
 if err != nil {
  fmt.Println("dial error ", err)
  return
 }
 defer conn.Close()
 // 實例化客戶端
 client := tslservice.NewTslServiceClient(conn)
 test, err := client.Test(context.TODO()&wrapperspb.Int32Value{Value: 1})
 fmt.Println("res:"test)
 fmt.Println("err:", err)
}

5.3 發起請求

# 證書正常時
$ go run client.go
res: code:200 msg:"success"
err: <nil>

# 故意寫錯客戶端中的 ServerName:test.com
$  go run client.go
res: <nil>
err: rpc error: code = Unavailable desc = connection error: desc = "transport: authentication handshake failed: x509: certificate is valid for liuqh.icu, not test.com"
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/1IvuGu9JvQ-oES-fEc5yJQ