Go gRPC 系列四:對 Client/Server 進行 TLS 校驗

前言

你們好,我是煎魚,在前面的章節裏,咱們介紹了 gRPC 的四種 API 使用方式。是否是很簡單呢。git

此時存在一個安全問題,先前的例子中 gRPC Client/Server 都是明文傳輸的,會不會有被竊聽的風險呢?github

從結論上來說,是有的。在明文通信的狀況下,你的請求就是裸奔的,有可能被第三方惡意篡改或者僞造爲「非法」的數據golang

抓個包

image

image

嗯,明文傳輸無誤。這是有問題的,接下將改造咱們的 gRPC,以便於解決這個問題 😤segmentfault

證書生成

私鑰

openssl ecparam -genkey -name secp384r1 -out server.key
複製代碼

自籤公鑰

openssl req -new -x509 -sha256 -key server.key -out server.pem -days 3650
複製代碼

填寫信息

Country Name (2 letter code) []:
State or Province Name (full name) []:
Locality Name (eg, city) []:
Organization Name (eg, company) []:
Organizational Unit Name (eg, section) []:
Common Name (eg, fully qualified host name) []:go-grpc-example
Email Address []:
複製代碼

生成完畢

生成證書結束後,將證書相關文件放到 conf/ 下,目錄結構:安全

$ tree go-grpc-example 
go-grpc-example
├── client
├── conf
│   ├── server.key
│   └── server.pem
├── proto
└── server
    ├── simple_server
    └── stream_server
複製代碼

因爲本文偏向 gRPC,詳解可參見 《製做證書》。後續番外可能會展開細節描述 👌bash

爲何以前不須要證書

在 simple_server 中,爲何「啥事都沒幹」就能在不須要證書的狀況下運行呢?服務器

Server

grpc.NewServer()
複製代碼

在服務端顯然沒有傳入任何 DialOptions網絡

Client

conn, err := grpc.Dial(":"+PORT, grpc.WithInsecure())
複製代碼

在客戶端留意到 grpc.WithInsecure() 方法app

func WithInsecure() DialOption {
	return newFuncDialOption(func(o *dialOptions) {
		o.insecure = true
	})
}
複製代碼

在方法內能夠看到 WithInsecure 返回一個 DialOption,而且它最終會經過讀取設置的值來禁用安全傳輸tcp

那麼它「最終」又是在哪裏處理的呢,咱們把視線移到 grpc.Dial() 方法內

func DialContext(ctx context.Context, target string, opts ...DialOption) (conn *ClientConn, err error) {
    ...
    
    for _, opt := range opts {
		opt.apply(&cc.dopts)
	}
    ...
    
    if !cc.dopts.insecure {
		if cc.dopts.copts.TransportCredentials == nil {
			return nil, errNoTransportSecurity
		}
	} else {
		if cc.dopts.copts.TransportCredentials != nil {
			return nil, errCredentialsConflict
		}
		for _, cd := range cc.dopts.copts.PerRPCCredentials {
			if cd.RequireTransportSecurity() {
				return nil, errTransportCredentialsMissing
			}
		}
	}
	...
	
	creds := cc.dopts.copts.TransportCredentials
	if creds != nil && creds.Info().ServerName != "" {
		cc.authority = creds.Info().ServerName
	} else if cc.dopts.insecure && cc.dopts.authority != "" {
		cc.authority = cc.dopts.authority
	} else {
		// Use endpoint from "scheme://authority/endpoint" as the default
		// authority for ClientConn.
		cc.authority = cc.parsedTarget.Endpoint
	}
	...
}
複製代碼

gRPC

接下來咱們將正式開始編碼,在 gRPC Client/Server 上實現 TLS 證書認證的支持 🤔

TLS Server

package main

import (
	"context"
	"log"
	"net"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"

	pb "github.com/EDDYCJY/go-grpc-example/proto"
)

...

const PORT = "9001"

func main() {
	c, err := credentials.NewServerTLSFromFile("../../conf/server.pem", "../../conf/server.key")
	if err != nil {
		log.Fatalf("credentials.NewServerTLSFromFile err: %v", err)
	}

	server := grpc.NewServer(grpc.Creds(c))
	pb.RegisterSearchServiceServer(server, &SearchService{})

	lis, err := net.Listen("tcp", ":"+PORT)
	if err != nil {
		log.Fatalf("net.Listen err: %v", err)
	}

	server.Serve(lis)
}
複製代碼
  • credentials.NewServerTLSFromFile:根據服務端輸入的證書文件和密鑰構造 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,用於設置服務器鏈接的憑據。用於 grpc.NewServer(opt ...ServerOption) 爲 gRPC Server 設置鏈接選項
func Creds(c credentials.TransportCredentials) ServerOption {
	return func(o *options) {
		o.creds = c
	}
}
複製代碼

通過以上兩個簡單步驟,gRPC Server 就創建起需證書認證的服務啦 🤔

TLS Client

package main

import (
	"context"
	"log"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"

	pb "github.com/EDDYCJY/go-grpc-example/proto"
)

const PORT = "9001"

func main() {
	c, err := credentials.NewClientTLSFromFile("../../conf/server.pem", "go-grpc-example")
	if err != nil {
		log.Fatalf("credentials.NewClientTLSFromFile err: %v", err)
	}

	conn, err := grpc.Dial(":"+PORT, grpc.WithTransportCredentials(c))
	if err != nil {
		log.Fatalf("grpc.Dial err: %v", err)
	}
	defer conn.Close()

	client := pb.NewSearchServiceClient(conn)
	resp, err := client.Search(context.Background(), &pb.SearchRequest{
		Request: "gRPC",
	})
	if err != nil {
		log.Fatalf("client.Search err: %v", err)
	}

	log.Printf("resp: %s", resp.GetResponse())
}
複製代碼
  • credentials.NewClientTLSFromFile():根據客戶端輸入的證書文件和密鑰構造 TLS 憑證。serverNameOverride 爲服務名稱
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) 設置鏈接選項
func WithTransportCredentials(creds credentials.TransportCredentials) DialOption {
	return newFuncDialOption(func(o *dialOptions) {
		o.copts.TransportCredentials = creds
	})
}
複製代碼

驗證

請求

從新啓動 server.go 和執行 client.go,獲得響應結果

$ go run client.go
2018/09/30 20:00:21 resp: gRPC Server
複製代碼

抓個包

image

成功。

總結

在本章節咱們實現了 gRPC TLS Client/Servert,你覺得大功告成了嗎?我不 😤

問題

你仔細再看看,Client 是基於 Server 端的證書和服務名稱來創建請求的。這樣的話,你就須要將 Server 的證書經過各類手段給到 Client 端,不然是沒法完成這項任務的

問題也就來了,你沒法保證你的「各類手段」是安全的,畢竟如今的網絡環境是很危險的,萬一被...

咱們將在下一章節解決這個問題,保證其可靠性。

若是有任何疑問或錯誤,歡迎在 issues 進行提問或給予修正意見,若是喜歡或對你有所幫助,歡迎 Star,對做者是一種鼓勵和推動。

個人公衆號

image

參考

本系列示例代碼

相關文章
相關標籤/搜索