跟我一起學Go系列:Go gRPC 安全認證機制-SSL/TLS認證

rickiyang發表於2021-07-07

Go gRPC 系列:

跟我一起學Go系列:gRPC 攔截器使用

跟我一起學Go系列:gRPC 入門必備

第一篇入門說過 gRPC 底層是基於 HTTP/2 協議的,HTTP 本身不帶任何加密傳輸功能,基於 SSL 的 HTTPS 協議才是加密傳輸。gRPC 使用了 HTTP/2 協議但是並未使用 HTTPS,即少了加密傳輸的部分。

對於加密傳輸的部分 gRPC 將它抽出來作為一個元件,可以由使用者自由選擇。gRPC 內預設提供了兩種 內建的認證方式:

  1. 基於 CA 證書的 SSL/TLS 認證方式;
  2. 基於 Token 的認證方式。

同時也提供了可擴充套件的使用者自定義認證方式。

gRPC 中的連線型別一共有以下 3 種:

  1. insecure connection:不使用 TLS 加密;
  2. server-side TLS:僅服務端 TLS 加密;
  3. mutual TLS:客戶端、服務端都使用 TLS 加密。

我們之前的例項中都是使用 insecure connection:

conn, err := grpc.Dial(":8972", grpc.WithInsecure())

這種方式相當於裸奔的資料在網路上行走,生產環境下這樣使用肯定是不行的。下面我們來說一下基於 TLS 認證方式加密操作。

server-side TLS

服務端 TLS 具體包含以下幾個步驟:

  1. 製作證書,包含服務端證書和 CA 證書;
  2. 服務端啟動時載入證書;
  3. 客戶端連線時使用CA 證書校驗服務端證書有效性。

CA 證書製作:

# 生成.key  私鑰檔案
$ openssl genrsa -out ca.key 2048

# 生成.csr 證書籤名請求檔案
$ openssl req -new -key ca.key -out ca.csr  -subj "/C=GB/L=China/O=rickiyang/CN=www.rickiyang.com"

# 自簽名生成.crt 證書檔案
$ openssl req -new -x509 -days 3650 -key ca.key -out ca.crt  -subj "/C=GB/L=China/O=rickiyang/CN=www.rickiyang.com"

服務端證書

和生成 CA證書類似,不過最後一步由 CA 證書進行簽名,而不是自簽名。

然後openssl 配置檔案可能位置不同,需要自己修改一下。

首先找到你的 openssl 位置:

$ find / -name "openssl.cnf"

然後生成簽名證書:

# 生成.key  私鑰檔案
$ openssl genrsa -out server.key 2048

# 生成.csr 證書籤名請求檔案
$ openssl req -new -key server.key -out server.csr \
	-subj "/C=GB/L=China/O=rickiyang/CN=www.rickiyang.com" \
	-reqexts SAN \
	-config <(cat /usr/local/etc/openssl@1.1/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:*.rickiyang.com"))

# 簽名生成.crt 證書檔案
$ openssl x509 -req -days 3650 \
   -in server.csr -out server.crt \
   -CA ca.crt -CAkey ca.key -CAcreateserial \
   -extensions SAN \
   -extfile <(cat /usr/local/etc/openssl@1.1/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:*.rickiyang.com"))

至此會生成如下檔案:

-rw-r--r--   1 rickiyang  staff  1119  6 30 10:32 ca.crt
-rw-r--r--   1 rickiyang  staff   964  6 30 10:32 ca.csr
-rw-r--r--   1 rickiyang  staff  1679  6 30 10:31 ca.key
-rw-r--r--   1 rickiyang  staff    17  6 30 10:34 ca.srl
-rw-r--r--   1 rickiyang  staff  1164  6 30 10:34 server.crt
-rw-r--r--   1 rickiyang  staff  1017  6 30 10:33 server.csr
-rw-r--r--   1 rickiyang  staff  1679  6 30 10:32 server.key

下面我們用到的會有這三個:

ca.crt
server.key
server.crt

下面來看一下如何將加密校驗邏輯加入到程式碼中。相關程式碼在 Github 上,自行下載檢視

服務端的修改點如下:

  • NewServerTLSFromFile 載入證書
  • NewServer 時指定 Creds。
func TestGrpcServer(t *testing.T) {
	// 監聽本地的8972埠
	lis, err := net.Listen("tcp", ":8972")
	if err != nil {
		fmt.Printf("failed to listen: %v", err)
		return
	}

	// TLS認證
	creds, err := credentials.NewServerTLSFromFile("/Users/rickiyang/server.crt", "/Users/rickiyang/server.key")
	if err != nil {
		grpclog.Fatalf("Failed to generate credentials %v", err)
	}

	//開啟TLS認證, 註冊攔截器
	s := grpc.NewServer(grpc.Creds(creds), grpc.UnaryInterceptor(LoggingInterceptor)) // 建立gRPC伺服器
	pb.RegisterGreeterServer(s, &server{})                         // 在gRPC服務端註冊服務

	reflection.Register(s) //在給定的gRPC伺服器上註冊伺服器反射服務
	// Serve方法在lis上接受傳入連線,為每個連線建立一個ServerTransport和server的goroutine。
	// 該goroutine讀取gRPC請求,然後呼叫已註冊的處理程式來響應它們。
	err = s.Serve(lis)
	if err != nil {
		fmt.Printf("failed to serve: %v", err)
		return
	}

}

同樣服務端使用 TLS 連線,客戶端也需要使用對應的連線方式才能進行傳輸。客戶端程式碼主要修改點:

  • NewClientTLSFromFile 指定使用 CA 證書來校驗服務端的證書有效性。注意:第二個引數域名就是前面生成服務端證書時指定的CN引數
  • 建立連線時 指定建立安全連線 WithTransportCredentials。

對應程式碼邏輯如下:

func TestGrpcClient(t *testing.T) {
	// TLS連線
	creds, err := credentials.NewClientTLSFromFile("/Users/rickiyang2/ca.crt", "www.rickiyang.com")
	if err != nil {
		grpclog.Fatalf("Failed to create TLS credentials %v", err)
	}

	// 連線伺服器
	conn, err := grpc.Dial(":8972", grpc.WithTransportCredentials(creds))
	if err != nil {
		fmt.Printf("faild to connect: %v", err)
	}
	defer conn.Close()

	c := pb.NewGreeterClient(conn)
	// 呼叫服務端的SayHello
	r, err := c.SayHello(context.Background(), &pb.HelloRequest{Name: "CN"})
	if err != nil {
		fmt.Printf("could not greet: %v", err)
	}

	fmt.Printf("Greeting: %s !\n", r.Message)
}
mutual TLS

server-side TLS 中雖然服務端使用了證書,但是客戶端卻沒有使用證書,本章節會給客戶端也生成一個證書,並完成 mutual TLS。

生成客戶端證書和生成服務端證書沒有什麼不同:

# 生成.key  私鑰檔案
openssl genrsa -out client.key 2048

# 生成.csr 證書籤名請求檔案
openssl req -new -key client.key -out client.csr \
	-subj "/C=GB/L=China/O=lixd/CN=www.rickiyang.com" \
	-reqexts SAN \
	-config <(cat /usr/local/etc/openssl@1.1/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:*.rickiyang.com"))

# 簽名生成.crt 證書檔案
openssl x509 -req -days 3650 \
-in client.csr -out client.crt \
-CA ca.crt -CAkey ca.key -CAcreateserial \
-extensions SAN \
-extfile <(cat /usr/local/etc/openssl@1.1/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:*.rickiyang.com"))

執行上面的命令之後我們會得到這兩個重要的檔案:

client.crt
client.key

下面就是在程式碼中去引用這些檔案,mutual TLS 配置客戶端和服務端都需要修改,相關程式碼點選檢視

具體改動如下:

服務端:

func TestGrpcServer(t *testing.T) {
	// 從證書相關檔案中讀取和解析資訊,得到證書公鑰、金鑰對
	certificate, err := tls.LoadX509KeyPair(data.Path("/Users/rickiyang2/server.crt"), data.Path("/Users/rickiyang2/server.key"))
	if err != nil {
		fmt.Errorf("err, %v", err)
	}
	// 建立CertPool,後續就用池裡的證書來校驗客戶端證書有效性
	// 所以如果有多個客戶端 可以給每個客戶端使用不同的 CA 證書,來實現分別校驗的目的
	certPool := x509.NewCertPool()
	ca, err := ioutil.ReadFile(data.Path("/Users/rickiyang2/ca.crt"))
	if err != nil {
		fmt.Errorf("err, %v", err)
	}
	if ok := certPool.AppendCertsFromPEM(ca); !ok {
		fmt.Errorf("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,
	})

	//開啟TLS認證, 註冊攔截器
	s := grpc.NewServer(grpc.Creds(creds), grpc.UnaryInterceptor(LoggingInterceptor)) // 建立gRPC伺服器
	pb.RegisterGreeterServer(s, &server{})                         // 在gRPC服務端註冊服務


	// 監聽本地的8972埠
	lis, err := net.Listen("tcp", ":8972")
	if err != nil {
		fmt.Printf("failed to listen: %v", err)
		return
	}

	reflection.Register(s) //在給定的gRPC伺服器上註冊伺服器反射服務
	// Serve方法在lis上接受傳入連線,為每個連線建立一個ServerTransport和server的goroutine。
	// 該goroutine讀取gRPC請求,然後呼叫已註冊的處理程式來響應它們。
	err = s.Serve(lis)
	if err != nil {
		fmt.Printf("failed to serve: %v", err)
		return
	}

}

客戶端:

func TestGrpcClient(t *testing.T) {

	// 載入客戶端證書
	certificate, err := tls.LoadX509KeyPair(data.Path("/Users/rickiyang2/client.crt"), data.Path("/Users/rickiyang2/client.key"))
	if err != nil {
		fmt.Errorf("err, %v", err)
	}
	// 構建CertPool以校驗服務端證書有效性
	certPool := x509.NewCertPool()
	ca, err := ioutil.ReadFile(data.Path("/Users/rickiyang2/ca.crt"))
	if err != nil {
		fmt.Errorf("err, %v", err)
	}
	if ok := certPool.AppendCertsFromPEM(ca); !ok {
		fmt.Errorf("failed to append ca certs")
	}

	creds := credentials.NewTLS(&tls.Config{
		Certificates: []tls.Certificate{certificate},
		ServerName:   "www.rickiyang.com", // NOTE: this is required!
		RootCAs:      certPool,
	})

	// 連線伺服器
	conn, err := grpc.Dial(":8972", grpc.WithTransportCredentials(creds))
	if err != nil {
		fmt.Printf("faild to connect: %v", err)
	}
	defer conn.Close()

	c := pb.NewGreeterClient(conn)
	// 呼叫服務端的SayHello
	r, err := c.SayHello(context.Background(), &pb.HelloRequest{Name: "CN"})
	if err != nil {
		fmt.Printf("could not greet: %v", err)
	}

	fmt.Printf("Greeting: %s !\n", r.Message)
}

本篇只介紹 SSL/TLS 認證相關的方式,生成證書相關操作本文是在 Mac 上操作,不同系統可能會有不一樣的環境問題, 如果出現問題按照相關提示排除錯誤。下一篇我們繼續介紹 Token 認證和自定義認證方式。

相關文章