07.gRPC的TLS认证

前面的章节实现的 gRPC 示例 Client/Server 之间的通信都是明文传输的,数据很容易被监听,甚至篡改。其实 gRPC 是鼓励开发者使用安全的通信的,只是在开始的章节中,客户端在创建连接时,使用 grpc.WithInsecure() 这个 Option 使 gRPC 以 Insecure 的方式连接。

本节主要介绍为 gRPC 添加 TLS 支持的过程,关于 TLS 的内容,请自行搜索相关资料。

本节的代码基于 grpc-testing v0.0.1

生成证书

$ cd grpc-testing-0.0.1
$ mkdir -p keys/{ca,server,client}

# CA

## 生成 ca.key 私钥文件
$ openssl genrsa -out keys/ca/ca.key 4096
## 生成 ca.csr 证书签名请求文件
$ openssl req -new -key keys/ca/ca.key \
    -out keys/ca/ca.csr \
    -subj "/O=cloudfstrife/CN=www.bitlogs.tech"
## 自签名生成 ca.crt 证书文件
$ openssl req -new -x509 -days 3650 \
    -key keys/ca/ca.key \
    -out keys/ca/ca.crt \
    -subj "/O=cloudfstrife/CN=www.bitlogs.tech"

# server
## 生成 server.key 私钥文件
$ openssl genrsa -out keys/server/server.key 4096
## 生成 SAN 证书签名请求文件 Go 1.15 版本开始废弃 CommonName 并且推荐使用 SAN 证书
$ openssl req -new -reqexts SAN \
    -key keys/server/server.key \
    -out keys/server/server.csr \
    -subj "/O=cloudfstrife/CN=www.bitlogs.tech" \
    -config <(cat /usr/lib/ssl/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:*.bitlogs.tech"))
## 使用 CA 证书对 server.csr 签名
$ openssl x509 -req -days 3650 \
    -extensions SAN \
    -in keys/server/server.csr \
    -out keys/server/server.crt \
    -CA keys/ca/ca.crt \
    -CAkey keys/ca/ca.key \
    -CAcreateserial \
    -extfile <(cat /etc/ssl/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:*.bitlogs.tech"))

# client

## 生成 client.key 私钥文件
$ openssl genrsa -out keys/client/client.key 4096
## 生成 SAN 证书签名请求文件
$ openssl req -new -reqexts SAN \
    -key keys/client/client.key \
    -out keys/client/client.csr \
    -subj "/O=cloudfstrife/CN=www.bitlogs.tech" \
    -config <(cat /usr/lib/ssl/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:*.bitlogs.tech"))
## 使用 CA 证书对 client.csr 签名
$ openssl x509 -req -days 3650 \
    -extensions SAN \
    -in keys/client/client.csr \
    -out keys/client/client.crt \
    -CA keys/ca/ca.crt \
    -CAkey keys/ca/ca.key \
    -CAcreateserial \
    -extfile <(cat /etc/ssl/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:*.bitlogs.tech"))

生成的Key如下:

keys
├── ca
│   ├── ca.crt
│   ├── ca.csr
│   ├── ca.key
│   └── ca.srl
├── client
│   ├── client.crt
│   ├── client.csr
│   └── client.key
└── server
    ├── server.crt
    ├── server.csr
    └── server.key

仅服务端 TLS

仅服务端的 TLS 实现过程主要由以下三步

  • 生成服务端证书
  • 服务端启动时加载 server 证书
  • 客户端创建连接时使用 CA 证书校验服务端证书

使用到的证书文件有以下几个:

  1. ca.crt
  2. server.key
  3. server.crt

拷贝证书到各目录

$ mkdir -p grpc-testing-{server,client}/cert
$ cp keys/server/server.{key,crt} grpc-testing-server/cert 
$ cp keys/ca/ca.crt grpc-testing-client/cert/

修改服务端代码

  1. 使用 credentials.NewServerTLSFromFile 创建 TransportCredentials
  2. NewServer 时,通过 grpc.Creds 指定 Creds
func main() {
	log.Println("Go")
	listen, err := net.Listen("tcp", ":8080")
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	// 创建 TransportCredentials
	creds, err := credentials.NewServerTLSFromFile("./cert/server.crt", "./cert/server.key")
	if err != nil {
		log.Fatal("create server TLS", err)
	}

	// 通过 Option 启动 TLS 支持
	s := grpc.NewServer(grpc.Creds(creds))
	echo.RegisterEchoServer(s, &EchoServer{})
	if err := s.Serve(listen); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

修改客户端代码

  1. 使用 credentials.NewClientTLSFromFile 创建 TransportCredentials
  2. 创建连接时,使用 grpc.WithTransportCredentials 指定建立安全连接
func main() {
	// 客户端通过 CA 证书来验证服务的提供的证书
	creds, err := credentials.NewClientTLSFromFile("./cert/ca.crt", "*.bitlogs.tech")
	if err != nil {
		log.Fatalf("failed to load credentials: %v", err)
	}
	// 建立连接时指定使用 TLS
	conn, err := grpc.Dial("127.0.0.1:8080", grpc.WithTransportCredentials(creds), grpc.WithBlock())
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()
	c := echo.NewEchoClient(conn)
	ctx, cancle := context.WithCancel(context.Background())
	defer cancle()
	r, err := c.Echo(ctx, &echo.EchoRequest{Msg: "hello"})
	if err != nil {
		log.Fatalf("echo failed :%#v", err)
	}
	log.Print(r.GetMsg())
}

双向 TLS

上面的示例,为 Server 启用了 TLS 功能,客户端却没有使用证书。下面的示例,为客户端也添加证书认证。

使用到的证书文件有以下几个:

  1. ca.crt
  2. server.key
  3. server.crt
  4. client.key
  5. client.crt

拷贝证书

$ cp keys/ca/ca.crt grpc-testing-server/cert/
$ cp keys/client/client.{key,crt} grpc-testing-client/cert 

修改服务端代码

  1. 使用 tls.LoadX509KeyPair 创建 Certificate
  2. 使用 x509.NewCertPool 创建用于验签的 CertPool 并把 CA 证书添加在其中
  3. credentials.NewTLS 使用创建 TransportCredentials
  4. NewServer 时,通过 grpc.Creds 指定 Creds
func main() {
	log.Println("Go")
	listen, err := net.Listen("tcp", ":8080")
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	// 创建 Certificate
	certificate, err := tls.LoadX509KeyPair("./cert/server.crt", "./cert/server.key")
	if err != nil {
		log.Fatal("create server TLS", err)
	}

	// 创建 CertPool 并把 CA 证书添加在其中
	p := x509.NewCertPool()
	ca, err := ioutil.ReadFile("./cert/ca.crt")
	if err != nil {
		log.Fatal("read ca.crt", err)
	}
	if !p.AppendCertsFromPEM(ca) {
		log.Fatal("append ca cert ")
	}

	// 创建 TransportCredentials
	creds := credentials.NewTLS(&tls.Config{
		// 证书链,可以是多个
		Certificates: []tls.Certificate{certificate},
		// 要求必须校验客户端的证书
		ClientAuth: tls.RequireAndVerifyClientCert,
		// 设置根证书的集合
		ClientCAs: p,
	})

	// 通过 Option 启动 TLS 支持
	s := grpc.NewServer(grpc.Creds(creds))
	echo.RegisterEchoServer(s, &EchoServer{})
	if err := s.Serve(listen); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

修改客户端代码

客户端的过程和服务端几乎没什么区别,只在创建 TransportCredentials 时的一些区别。

  1. 使用 tls.LoadX509KeyPair 创建 Certificate
  2. 使用 x509.NewCertPool 创建用于验签的 CertPool 并把 CA 证书添加在其中
  3. credentials.NewTLS 使用创建 TransportCredentials
  4. 创建连接时,使用 grpc.WithTransportCredentials 指定建立安全连接
func main() {
	// 创建 Certificate
	certificate, err := tls.LoadX509KeyPair("./cert/client.crt", "./cert/client.key")
	if err != nil {
		log.Fatal("load x509 key pair", err)
	}

	// 创建 CertPool 并把 CA 证书添加在其中
	p := x509.NewCertPool()
	ca, err := ioutil.ReadFile("./cert/ca.crt")
	if err != nil {
		log.Fatal("read ca.crt", err)
	}
	if !p.AppendCertsFromPEM(ca) {
		log.Fatal("append ca cert ")
	}

	// 创建 TransportCredentials
	creds := credentials.NewTLS(&tls.Config{
		// 证书链,可以是多个
		Certificates: []tls.Certificate{certificate},
		// 要求必须校验客户端的证书
		ServerName: "*.bitlogs.tech",
		// 设置根证书的集合
		RootCAs: p,
	})

	// 建立连接时指定使用 TLS
	conn, err := grpc.Dial("127.0.0.1:8080", grpc.WithTransportCredentials(creds), grpc.WithBlock())
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()
	c := echo.NewEchoClient(conn)
	ctx, cancle := context.WithCancel(context.Background())
	defer cancle()
	r, err := c.Echo(ctx, &echo.EchoRequest{Msg: "hello"})
	if err != nil {
		log.Fatalf("echo failed :%#v", err)
	}
	log.Print(r.GetMsg())
}