TLS 服務端認證

使用 CFSSL 生成自己的 CA 正式

在修改服務端代碼之前,我們先創建幾個證書。我們可以通過第三方認證機構獲取證書,但那需要花錢而且麻煩。對於內部服務的話,沒必要通過第三方機構。受信任的證書不一定來自於 Comodo 或 Let 's Encrypt 或任何其他權威 CA 機構,它們可以來自於自己生成的 CA。只要有合適的工具,這是免費也容易做到的。

CloudFlare 公司 (一家美國雲計算公司) 開發了一套工具包叫 CFSSL,用於簽名、認證和 TLS 證書綁定。CloudFlare 使用 CFSSL 爲內部服務提供 TLS 證書,扮演內部服務的權威機構發放證書。而且該公司將 CFSSL 開源了,因此其他人包括我們也可以使用。甚至是其他權威機構,像 Let’s Encrypt 公司也使用 CFSSL。感謝 CloudFlare,因爲 CFSSL 確實是一個非常有用的工具。

CFSSL 有兩個工具是我們需要的:

通過以下命令安裝 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:是指被添加到證書中的各種名稱信息的列表。每個名稱對象至少包含一個值,它們表示:

創建 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。以下是不同的配置:

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