gRPC(五)進階:透過TLS建立安全連線

lin鍾一發表於2022-11-09

個人網站:linzyblog.netlify.app/
示例程式碼已經上傳到github:點選跳轉
gRPC官方文件:點選跳轉

先前的例子中 gRPC Client/Server 都是明文傳輸的,在明文通訊的情況下,你的請求就是裸奔的,有可能被第三方惡意篡改或者偽造為“非法”的資料。

我們抓個包檢視一下:
在這裡插入圖片描述
在這裡插入圖片描述
是明文傳輸,後面我們開始gRPC透過 TLS 證照建立安全連線,讓資料能夠加密處理,包括證照製作和CA簽名校驗等。

傳輸層安全 (TLS) 對透過 Internet 傳送的資料進行加密,以確保竊聽者和駭客無法看到您傳輸的內容,這對於密碼、信用卡號和個人通訊等私人和敏感資訊特別有用。

1、什麼是TLS?

傳輸層安全 (TLS) 是一種 Internet 工程任務組 ( IETF ) 標準協議,可在兩個通訊計算機應用程式之間提供身份驗證、隱私和資料完整性。它是當今使用最廣泛部署的安全協議,最適合需要透過網路安全交換資料的 Web 瀏覽器和其他應用程式。這包括 Web 瀏覽會話、檔案傳輸、虛擬專用網路 (VPN) 連線、遠端桌面會話和 IP 語音 (VoIP)。最近,TLS 被整合到包括 5G 在內的現代蜂窩傳輸技術中,以保護整個無線電接入網路 ( RAN ) 的核心網路功能。

2、TLS的工作流程

TLS 使用客戶端-伺服器握手機制來建立加密和安全的連線,並確保通訊的真實性。

  • 通訊裝置交換加密功能。
  • 使用數字證照進行身份驗證過程以幫助證明伺服器是它聲稱的實體。
  • 發生會話金鑰交換。在此過程中,客戶端和伺服器必須就金鑰達成一致,以建立安全會話確實在客戶端和伺服器之間的事實——而不是在中間試圖劫持會話的東西。

在這裡插入圖片描述

1、概述

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

gRPC 內建了以下 encryption 機制:

  • SSL / TLS:透過證照進行資料加密;
  • ALTS:Google開發的一種雙向身份驗證和傳輸加密系統。
    • 只有執行在 Google Cloud Platform 才可用,一般不用考慮。

2、gRPC 加密型別

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

我們前面的例子都是明文傳輸的,使用的都是 insecure connection,透過指定 WithInsecure option 來建立 insecure connection,不建議在生產環境使用。

後面我們瞭解如何使用 TLS 來建立安全連線。

3、server-side TLS

1)流程

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

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

也可以不使用 CA證照,即服務端證照自簽名。

2)什麼是CA?CA證照又是什麼?

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

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

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

    • 身份資訊: 用於證明證照持有者的身份
    • CA機構的簽名: 用於保證身份的真實性
    • 公鑰和私鑰: 用於通訊過程中加解密,從而保證通訊資訊的安全性

3)什麼是SAN?

SAN(Subject Alternative Name)是 SSL 標準 x509 中定義的一個擴充套件。使用了 SAN 欄位的 SSL 證照,可以擴充套件此證照支援的域名,使得一個證照可以支援多個不同域名的解析。

我們在用go 1.15版本以上,用gRPC透過TLS建立安全連線時,會出現證照報錯問題:

panic: rpc error: code = Unavailable desc = connection error: desc = "transport: authentication handshake failed: x509: certificate
is not valid for any names, but wanted to match localhost"

造成這個panic的原因是從go 1.15 版本開始廢棄 CommonName,我們沒有使用官方推薦的 SAN 證照(預設是沒有開啟SAN擴充套件)而出現的錯誤,導致客戶端和服務端無法建立連線。

4)目錄結構

go-grpc-example
├── client
│      └──TLS_client
│   │   └──client.go
├── conf
│   └──ca.conf
│   └──server.conf
├── proto
│   └──search
│   │   └──search.proto
├── server
│      └──TLS_server
│   │   └──server.go
├── Makefile

5)生成CA根證照

在ca.conf裡寫入內容如下:

[ req ]
default_bits       = 4096
distinguished_name = req_distinguished_name

[ req_distinguished_name ]
countryName                 = GB
countryName_default         = CN
stateOrProvinceName         = State or Province Name (full name)
stateOrProvinceName_default = ZheJiang
localityName                = Locality Name (eg, city)
localityName_default        = HuZhou
organizationName            = Organization Name (eg, company)
organizationName_default    = Step
commonName                  = linzyblog.netlify.app
commonName_max              = 64
commonName_default          = linzyblog.netlify.app
  1. 生成ca私鑰,得到ca.key
openssl genrsa -out ca.key 4096

openssl genrsa:生成RSA私鑰,命令的最後一個引數,將指定生成金鑰的位數,如果沒有指定,預設512

  1. 生成ca證照籤發請求,得到ca.csr
$ openssl req -new -sha256 -out ca.csr -key ca.key -config ca.conf
GB [CN]:
State or Province Name (full name) [ZheJiang]:
Locality Name (eg, city) [HuZhou]:
Organization Name (eg, company) [Step]:
linzyblog.netlify.app [linzyblog.netlify.app]:

這裡一直回車就好了
openssl req:生成自簽名證照,-new指生成證照請求、-sha256指使用sha256加密、-key指定私鑰檔案、-x509指輸出證照、-days 3650為有效期,此後則輸入證照擁有者資訊

  1. 生成ca根證照,得到ca.crt
openssl x509 -req -days 3650 -in ca.csr -signkey ca.key -out ca.crt

在這裡插入圖片描述

6)生成終端使用者證照

在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 = ZheJiang
localityName                = Locality Name (eg, city)
localityName_default        = HuZhou
organizationName            = Organization Name (eg, company)
organizationName_default    = Step
commonName                  = CommonName (e.g. server FQDN or YOUR name)
commonName_max              = 64
commonName_default          = linzyblog.netlify.app

[ req_ext ]
subjectAltName = @alt_names

[alt_names]
DNS.1   = go-grpc-example(這裡很重要,客戶端需要此欄位做匹配)
IP      = 127.0.0.1
  1. 生成私鑰,得到server.key
openssl genrsa -out server.key 4096
  1. 生成證照籤發請求,得到server.csr
openssl req -new -sha256 -out server.csr -key server.key -config server.conf

這裡也一直回車就好。

  1. 用CA證照生成終端使用者證照,得到server.crt
openssl x509 -req -days 3650 -CA ca.crt -CAkey ca.key -CAcreateserial -in server.csr -out server.pem -extensions req_ext -extfile server.conf

在這裡插入圖片描述

7)server

const PORT = "8888"

func main() {
    // 根據服務端輸入的證照檔案和金鑰構造 TLS 憑證
    c, err := credentials.NewServerTLSFromFile("./conf/server.pem", "./conf/server.key")
    if err != nil {
        log.Fatalf("credentials.NewServerTLSFromFile err: %v", err)
    }
    // 返回一個 ServerOption,用於設定伺服器連線的憑據。
    // 用於 grpc.NewServer(opt ...ServerOption) 為 gRPC Server 設定連線選項
    lis, err := net.Listen("tcp", ":"+PORT) //建立 Listen,監聽 TCP 埠
    if err != nil {
        log.Fatalf("credentials.NewServerTLSFromFile err: %v", err)
    }
    search.RegisterSearchServiceServer(s, &service{})

    s.Serve(lis)
}

8)client

const PORT = "8888"

func main() {
    // 根據客戶端輸入的證照檔案和金鑰構造 TLS 憑證。
    // 第二個引數 serverNameOverride 為服務名稱。
    c, err := credentials.NewClientTLSFromFile("./conf/server.pem", "go-grpc-example")
    if err != nil {
        log.Fatalf("credentials.NewClientTLSFromFile err: %v", err)
    }
    // 返回一個配置連線的 DialOption 選項。
    // 用於 grpc.Dial(target string, opts ...DialOption) 設定連線選項
    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())
}

8)啟動 & 請求

# 啟動服務端
$ go run server.go
API server listening at: 127.0.0.1:53981

# 啟動客戶端
$ go run client.go 
API server listening at: 127.0.0.1:54328
2022/11/03 19:35:10 resp: gRPC Server

抓個包再看看
在這裡插入圖片描述

4、mutual TLS

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 = ZheJiang
localityName                = Locality Name (eg, city)
localityName_default        = HuZhou
organizationName            = Organization Name (eg, company)
organizationName_default    = Step
commonName                  = CommonName (e.g. server FQDN or YOUR name)
commonName_max              = 64
commonName_default          = linzyblog.netlify.app

[ req_ext ]
subjectAltName = @alt_names

[alt_names]
DNS.1   = go-grpc-example(這裡很重要,客戶端需要此欄位做匹配)
IP      = 127.0.0.1
// 1. 生成私鑰,得到server.key
openssl genrsa -out server.key 2048

//2. 生成證照籤發請求,得到server.csr
openssl req -new -sha256 -out server.csr -key server.key -config server.conf

//3. 用CA證照生成終端使用者證照,得到server.crt
openssl x509 -req -sha256 -CA ca.crt -CAkey ca.key -CAcreateserial -days 365 -in server.csr -out se
rver.crt -extensions req_ext -extfile server.conf

2)生成客戶端證照

// 1. 生成私鑰,得到client.key
openssl genrsa -out client.key 2048

//2. 生成證照籤發請求,得到client.csr
openssl req -new -key client.key -out client.csr 


//3. 用CA證照生成客戶端證照,得到client.crt
 openssl x509 -req -sha256 -CA ca.crt -CAkey ca.key -CAcreateserial -days 365  -in client.csr -out client.crt

在這裡插入圖片描述

3)整理目錄

conf
├── ca.conf
├── ca.crt
├── ca.csr
├── ca.key
├── client
│   ├── client.csr
│   ├── client.key
│   └── client.pem
├── server
│   ├── server.conf
|   └── server.crt
│   ├── server.csr
|   ├── server.key
└─server_side_TLS

4)server

const PORT = "8888"

func main() {
    // 公鑰中讀取和解析公鑰/私鑰對
    cert, err := tls.LoadX509KeyPair("./conf/server/server.crt", "./conf/server/server.key")
    if err != nil {
        fmt.Println("LoadX509KeyPair error", err)
        return
    }
    // 建立一組根證照
    certPool := x509.NewCertPool()
    ca, err := ioutil.ReadFile("./conf/ca.crt")
    if err != nil {
        fmt.Println("read ca pem error ", err)
        return
    }
    // 解析證照
    if ok := certPool.AppendCertsFromPEM(ca); !ok {
        fmt.Println("AppendCertsFromPEM error ")
        return
    }

    c := credentials.NewTLS(&tls.Config{
        //設定證照鏈,允許包含一個或多個
        Certificates: []tls.Certificate{cert},
        //要求必須校驗客戶端的證照
        ClientAuth: tls.RequireAndVerifyClientCert,
        //設定根證照的集合,校驗方式使用ClientAuth設定的模式
        ClientCAs: certPool,
    })
    s := grpc.NewServer(grpc.Creds(c))
    lis, err := net.Listen("tcp", ":"+PORT) //建立 Listen,監聽 TCP 埠
    if err != nil {
        log.Fatalf("credentials.NewServerTLSFromFile err: %v", err)
    }
    //將 SearchService(其包含需要被呼叫的服務端介面)註冊到 gRPC Server 的內部註冊中心。
    //這樣可以在接受到請求時,透過內部的服務發現,發現該服務端介面並轉接進行邏輯處理
    search.RegisterSearchServiceServer(s, &service{})

    //gRPC Server 開始 lis.Accept,直到 Stop 或 GracefulStop
    s.Serve(lis)
}

5)client

const PORT = "8888"

func main() {
    // 公鑰中讀取和解析公鑰/私鑰對
    cert, err := tls.LoadX509KeyPair("./conf/client/client.crt", "./conf/client/client.key")
    if err != nil {
        fmt.Println("LoadX509KeyPair error ", err)
        return
    }
    // 建立一組根證照
    certPool := x509.NewCertPool()
    ca, err := ioutil.ReadFile("./conf/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
    }

    c := credentials.NewTLS(&tls.Config{
        Certificates: []tls.Certificate{cert},
        ServerName:   "go-grpc-example",
        RootCAs:      certPool,
    })

    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())
}

6)啟動 & 請求

# 啟動服務端
$ go run server.go
API server listening at: 127.0.0.1:56036

# 啟動客戶端
$ go run client.go 
API server listening at: 127.0.0.1:56364
2022/11/03 20:21:55 resp: gRPC Server

# 更改ServerName為linzy
$ go run client.go 
API server listening at: 127.0.0.1:56424
2022/11/03 20:23:17 client.Search err: rpc error: code = Unavailable desc = connection error: desc = "transport: authentication handshake failed: x509: cer
tificate is valid for go-grpc-example, not linzy"

抓個包看看
在這裡插入圖片描述
在這裡插入圖片描述

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章