TLS 服務端認證
使用 CFSSL 生成自己的 CA 正式
在修改服務端代碼之前,我們先創建幾個證書。我們可以通過第三方認證機構獲取證書,但那需要花錢而且麻煩。對於內部服務的話,沒必要通過第三方機構。受信任的證書不一定來自於 Comodo 或 Let 's Encrypt 或任何其他權威 CA 機構,它們可以來自於自己生成的 CA。只要有合適的工具,這是免費也容易做到的。
CloudFlare 公司 (一家美國雲計算公司) 開發了一套工具包叫 CFSSL,用於簽名、認證和 TLS 證書綁定。CloudFlare 使用 CFSSL 爲內部服務提供 TLS 證書,扮演內部服務的權威機構發放證書。而且該公司將 CFSSL 開源了,因此其他人包括我們也可以使用。甚至是其他權威機構,像 Let’s Encrypt 公司也使用 CFSSL。感謝 CloudFlare,因爲 CFSSL 確實是一個非常有用的工具。
CFSSL 有兩個工具是我們需要的:
-
cfssl 用於簽名、驗證和綁定 TLS 證書,並將結果輸出爲 JSON。
-
cfssljson 獲取 JSON 輸出,並將其拆分爲單獨的密鑰、證書、CSR。
通過以下命令安裝 CloudFlare 的命令行工具:
$ go get github.com/cloudflare/cfssl/cmd/cfssl@v1.4.1
$ go get github.com/cloudflare/cfssl/cmd/cfssljson@v1.4.1
爲了初始化 CA 並生成 cert,我們需要將各種配置文件傳給 cfssl 命令。需要單獨的配置文件來生成 CA 和服務器端證書,我們需要一個包含關於 CA 信息的配置文件。所以先通過運行 $ mkdir test 在項目中創建一個目錄來存放這些配置文件。
proglog/test/ca-csr.json
{
"CN": "My Awesome CA",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "CA",
"L": "ON",
"ST": "Toronto",
"O": "My Awesome Company",
"OU": "CA Services"
}
]
}
cfssl 工具根據這個 json 文件來配置 CA 證書。CN 表示通用名稱 (Common Name),因此這裏稱我們的 CA 證書爲 “My Awesome CA”。key 指定用於簽署證書的算法和密鑰大小。names:是指被添加到證書中的各種名稱信息的列表。每個名稱對象至少包含一個值,它們表示:
-
C - 國家
-
L - 所屬地或市區
-
ST - 州或者省
-
O - 組織
-
OU 組織單位 (例如持有證書的部門)
創建 test/ca-config.json 文件,用於定義 CA 的策略,如下所示:
{
"signing": {
"profiles": {
"server": {
"expiry": "8760h",
"usages": [
"signing",
"key encipherment",
"server auth"
]
},
"client": {
"expiry": "8760h",
"usages": [
"signing",
"key encipherment",
"client auth"
]
}
}
}
}
CA 機構需要知道發佈的是哪種證書。其中 signing 部分定義 CA 的簽名策略。配置文件表明 CA 可以生成客戶端和服務器證書,這些證書將在一年後過期,並且這些證書可以用於數字簽名、加密密匙和認證。
將以下 json 內容添加到 server-csr.json 文件並放在 test 目錄中:
{
"CN": "127.0.0.1",
"hosts": [
"127.0.0.1",
"localhost"
],
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "CA",
"L": "ON",
"ST": "Toronto",
"O": "My Awesome Company",
"OU": "Distributed Services"
}
]
}
cfssl 將使用該配置文件來生產服務端證書。文件中的 hosts 字段是服務端域名列表,也是證書生效的域名列表。因爲我們在本地運行服務,只需要 127.0.0.1 和 localhost。
接下來更新項目的 Makefile 調用 cfssl 和 cfssjson 來生成證書,更新 Makefile 如下:
CONFIG_PATH=${HOME}/.proglog/
init:
mkdir -p ${CONFIG_PATH}
.PHONY: gencert
gencert:
cfssl gencert -initca test/ca-csr.json | cfssljson -bare ca
cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=test/ca-config.json -profile=server test/server-csr.json | cfssljson -bare server
mv *.pem *.csr ${CONFIG_PATH}
在更新後的 Makefile 中,我們添加了 CONFIG_PATH 變量指定證書存放位置,並通過 init 來創建對應的目錄。gencert:調用 cfssl 來生成 CA 證書和私鑰。
我們將在測試中頻繁使用這些證書文件,所以創建一個讀取證書的包,其中包含文件路徑作爲變量,以便於引用它們。在 internal/config 目錄中創建 files.go 文件,包含以下代碼:
package config
import (
"os"
"path/filepath"
)
var (
CAFile = configFile("ca.pem")
ServerCertFile = configFile("server.pem")
ServerKeyFile = configFile("server-key.pem")
)
func configFile(fileName string) string {
if dir := os.Getenv("CONFIG_DIR"); dir != "" {
return filepath.Join(dir, fileName)
}
homeDir, err := os.UserHomeDir()
if err != nil {
panic(err)
}
return filepath.Join(homeDir, "/.proglog/", fileName)
}
這些變量定義了生成的證書文件路徑,便於在代碼中查找和解析。這裏使用常量通過函數調用找到對應的證書文件。
有了這些證書就可以創建 * tls.Config 對象,爲此創建一個輔助函數和結構體。在 config 目錄中,創建 tls.go 文件,添加以下代碼:
package config
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
)
type TLSConfig struct {
CertFile string
KeyFile string
CAFile string
ServerAddress string
Server bool
}
func SetupTLSConfig(cfg TLSConfig) (*tls.Config, error) {
var err error
tlsConfig := &tls.Config{}
if cfg.CertFile != "" && cfg.KeyFile != "" {
tlsConfig.Certificates = make([]tls.Certificate, 1)
tlsConfig.Certificates[0], err = tls.LoadX509KeyPair(cfg.CertFile, cfg.KeyFile)
if err != nil {
return nil, err
}
}
if cfg.CAFile != "" {
b, err := ioutil.ReadFile(CAFile)
if err != nil {
return nil, err
}
ca := x509.NewCertPool()
ok := ca.AppendCertsFromPEM([]byte(b))
if !ok {
return nil, fmt.Errorf("failed to parse root certifacte: %q", cfg.CAFile)
}
if cfg.Server {
tlsConfig.ClientCAs = ca
tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
} else {
tlsConfig.RootCAs = ca
}
tlsConfig.ServerName = cfg.ServerAddress
}
return tlsConfig, nil
}
我們的測試使用了幾種不同的 tls.Config 配置,並且 SetupTLSConfi 函數允許我們使用一個函數獲得每種類型的 tls.Config。以下是不同的配置:
-
客戶端 * tls.Config:通過客戶端證書和 RootCAs 根證書來驗證服務端證書。
-
客戶端 * tls.Config:驗證服務端證書並允許服務端根據 RootCAs 和服務端證書來驗證客戶端證書。
-
服務端 * tls.Config:用於驗證客戶端證書,並通過設置客戶端 CAs、證書和 tls.RequireAndVerifyClientCert 開啓客戶端認證模式,允許客戶端驗證服務端證書。
TLSConfig 結構體定義了 SetupTLSConfig 函數參數,決定了 * tls.Config 的類型。
回到我們之前的測試用例,測試客戶端使用 CA 證書來驗證服務端證書。如果服務端證書是來自不同的 CA 機構,客戶端是不會信任服務端就不會創建 tls 連接。在 set_test.go 文件中,修改以下代碼:
proglog/internal/server/server_test.go
t.Helper()
l, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
clientTLSConfig, err := config.SetupTLSConfig(config.TLSConfig{
CAFile: config.CAFile,
})
require.NoError(t, err)
clientCreds := credentials.NewTLS(clientTLSConfig)
cc, err := grpc.Dial(l.Addr().String(), grpc.WithTransportCredentials(clientCreds))
require.NoError(t, err)
client := api.NewLogClient(cc)
在這段代碼中,配置客戶端的 TLS 使用 CA 證書作爲客戶端的根 CA 證書 (它將使用該 CA 驗證服務器證書)。然後,我們告訴客戶端使用這些憑證進行連接。
接下來,我們需要將服務端與它的證書關聯起來,並使其能夠處理 TLS 連接。在前面的代碼片段下面添加以下代碼:
serverTLSConfig, err := config.SetupTLSConfig(config.TLSConfig{
CAFile: config.CAFile,
CertFile: config.ServerCertFile,
KeyFile: config.ServerKeyFile,
ServerAddress: l.Addr().String(),
Server: true,
})
require.NoError(t, err)
serverCreds := credentials.NewTLS(serverTLSConfig)
dir, err := ioutil.TempDir("", "server-test")
require.NoError(t, err)
cLog, err := log.NewLog(dir, log.Config{})
require.NoError(t, err)
cfg = &Config{CommitLog: cLog}
if fn != nil {
fn(cfg)
}
server, err := NewGRPCServer(cfg, grpc.Creds(serverCreds))
require.NoError(t, err)
go func() {
server.Serve(l)
}()
return client, cfg, func(){
server.Stop()
cc.Close()
l.Close()
}
在這段代碼中,解析服務器的證書和密鑰,然後使用它們配置服務器的 TLS 憑證。我們將這些憑證作爲 gRPC 服務器選項傳遞給 NewGRPCServer 函數,這樣它就可以用該選項創建 gRPC 服務器。gRPC 服務器選項是你在 gRPC 服務器中啓用相應功能的方式。在本例中,我們爲服務器連接設置證書,但還有許多其他服務器選項 [25] 來配置連接超時、keeplive 策略等。
最後,我們需要更新服務器中的 NewGRPCServer 函數。去接受給定的 gRPC 服務器選項,並使用它們創建服務器。將 NewGRPCServer 函數修改爲:
func NewGRPCServer(config *Config, opts ...grpc.ServerOption) (*grpc.Server, error) {
gsrv := grpc.NewServer(opts...)
srv, err := newgrpcServer(config)
if err != nil {
return nil, err
}
api.RegisterLogServer(gsrv, srv)
return gsrv, nil
}
此時,您可以使用 $ make test 運行測試,測試應該會通過。不同的是,你的服務器現在是經過認證的,連接是加密的。你可以通過將測試代碼臨時更改爲使用帶有 grpc.WithInsecure() 選項的不安全客戶端連接來驗證這一點,再次運行測試。測試將失敗,客戶端和服務器將無法彼此連接,因爲服務器期望客戶端通過 TLS 運行。
服務器經過認證後,你就知道客戶端正在連接的服務器是真實的,而不是某個中間人。後面我們將使用雙向 TLS 認證來驗證連接服務器的客戶端真實身份。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/xDtxz0Do-3H42d_GyBElTg