传输层安全协议(transport layer security,TLS)旨在为两个应用程序之间的通信提供隐私性和数据完整性。在这里,它可用于在 gRPC 客户端应用程序和服务器端应用程序之间提供安全连接。根据传输层安全协议规范,如果客户端和服务器端之间的连接是安全的,那么它应该具备以
下一项或两项特性。
连接是私密的
使用对称加密的方式进行数据加密。在这种类型的加密中,只用一个密钥加密和解密。对于每个连接,这些密钥都是唯一的,它们是基于会话开始时所协商的共享密钥生成的。
连接是可靠的
这之所以能够实现,是因为每条消息都包含消息完整性检查,以防止在传输期间出现未被检测到的数据丢失或数据修改。
可见,通过安全的连接发送数据非常重要。借助 TLS
保护 gRPC
连接并不难,因为这种认证机制内置在了 gRPC
库中,它还促使我们使用 TLS对数据交换进行认证和加密。
我们该如何在 gRPC 连接中启用 TLS 呢?客户端和服务器端之间的安全数据传输可以采用单向或双向(也称为相互 TLS 或 mTLS)的方式来实现。下面讨论如何按照每种方式启用 TLS。
6.1.1 启用单向安全连接
在单向连接中,只有客户端会校验服务器端,以确保它所接收的数据来自预期的服务器。在建立连接时,服务器端会与客户端共享其公开证书,客户端则会校验接收到的证书。这是通过证书授权中心(certificateauthority,CA)完成的,也就是 CA 签署的证书。证书校验之后,客户端会发送使用密钥加密的数据。
CA 是一个受信任的实体,它管理和发布用于公共网络中安全通信的安全证书和公钥。由该受信任实体所签署或颁发的证书称为 CA 签名的证书。
要启用 TLS,首先需要创建以下证书和密钥。
server.key
RSA 私钥,用于签名和认证公钥。server.pem/server.crt
用于分发的自签名 X.509 公钥。
RSA 是其三位发明者的首字母组成的缩写:Rivest、Shamir和 Adleman。RSA 是最流行的公钥密码系统之一,广泛应用于安全数据传输。在 RSA 中有一个每个人都可以知道的公钥来加密数据,一个私钥来解密数据。其理念是,使用公钥加密的消息只能在合理的时间内通过私钥解密。
为了生成密钥,我们可以使用 OpenSSL,这是一个适用于 TLS 和安全套接字层(secure socket layer,SSL)协议的开源工具集。它能够生成具有不同长度和密码的私钥以及公开证书等。另外,还有其他一些工具,如 mkcert 和 certstrap,它们也能很容易地生成密钥和证书。
这里不会详细描述如何生成自签名证书的密钥,因为生成这些密钥和证书的详细步骤在源代码仓库的 README 文件中进行了描述。
假设我们已经创建了私钥和公共证书,接下来将它们用于第 1 章和第 2章所讨论的在线商品管理系统,并保护 gRPC 服务器端和客户端之间的通信。
01. 在 gRPC 服务器端启用单向安全连接
这是加密客户端和服务器端通信的最简单的方式。在这里,服务器端需要使用一个公钥–私钥对进行初始化。我们将阐述如何使用gRPC Go 服务器实现这一点。
为了启用安全的 Go 服务器,需要更新服务器实现的主函数,如代码清单 6-1 所示。
代码清单 6-1 用于托管 ProductInfo 服务的安全 gRPC 服务器实现
package main
import (
"crypto/tls"
"errors"
pb "productinfo/server/ecommerce"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"log"
"net"
)
var (
port = ":50051"
crtFile = "server.crt"
keyFile = "server.key"
)
func main() {
cert, err := tls.LoadX509KeyPair(crtFile,keyFile) ➊
if err != nil {
log.Fatalf("failed to load key pair: %s", err)
}
opts := []grpc.ServerOption{
grpc.Creds(credentials.NewServerTLSFromCert(&cert)) ➋
}
s := grpc.NewServer(opts...) ➌
pb.RegisterProductInfoServer(s, &server{}) ➍
lis, err := net.Listen("tcp", port) ➎
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
if err := s.Serve(lis); err != nil { ➏
log.Fatalf("failed to serve: %v", err)
}
}
- ❶ 读取和解析公钥–私钥对,并创建启用 TLS 的证书。
- ❷ 添加证书作为 TLS 服务器凭证,从而为所有传入的连接启用TLS。
- ❸ 通过传入 TLS 服务器凭证来创建新的 gRPC 服务器实例。
- ❹ 通过调用生成的 API,将服务实现注册到新创建的 gRPC 服务器上。
- ❺ 在端口 50051 上创建 TCP 监听器。
- ❻ 绑定 gRPC 服务器到监听器,并开始监听端口 50051 上传入的消息。
现在已经修改了服务器,使其能够接收来自客户端的请求,客户端可以验证服务器的证书。再修改一下客户端代码,使其能够与服务器“交流”。
02. 在 gRPC 客户端启用单向安全连接
为了与服务器连接,客户端需要服务器端的自认证公钥。我们可以修改 Go 的客户端代码以连接服务器,如代码清单 6-2 所示。
代码清单 6-2 安全的 gRPC 客户端应用程序
package main
import (
"log"
pb "productinfo/server/ecommerce"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc"
)
var (
address = "localhost:50051"
hostname = "localhost
crtFile = "server.crt"
)
func main() {
creds, err := credentials.NewClientTLSFromFile(crtFile, hostname) ➊
if err != nil {
log.Fatalf("failed to load credentials: %v", err)
}
opts := []grpc.DialOption{
grpc.WithTransportCredentials(creds), ➋
}
conn, err := grpc.Dial(address, opts...) ➌
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close() ➎
c := pb.NewProductInfoClient(conn) ➍
... // 省略了RPC方法调用
}
- ❶ 读取并解析公开证书,创建启用 TLS 的证书。
- ❷ 以 DialOption 的形式添加传输凭证。
- ❸ 通过传入 dial 选项,建立到服务器的安全连接。
- ❹ 传入连接并创建存根。该存根实例包含了调用服务器的所有远程方法。
- ❺ 所有事情完成后关闭连接。
这是一个非常简单直接的过程。只需添加 3 行代码并修改原始代码中的一行代码就可以了。首先,根据服务器端的公钥文件创建凭据对象,然后将传输凭证传递到 gRPC dialer 中,这样客户端每次建立与服务器之间的连接时就会启用 TLS 握手。
在单向 TLS 中,我们只认证服务器的身份。下一节会对双方(客户端和服务器端)都进行认证。
6.1.2 启用mTLS保护的连接
客户端和服务器端采用 mTLS 连接的主要目的是,控制能够连接服务器端的客户端。与单向安全连接不同,这种方式会将服务器配置为仅接受来自一组范围有限、已验证的客户端的连接。在这种方式中,双方彼此共享公开证书,并校验对方的身份。连接的基本流程如下所示。
- 客户端发送一个请求,试图访问服务器端受保护的信息。
- 服务器端发送它的 X.509 证书给客户端。
- 客户端通过 CA 对接收到的证书进行校验,判断是否为 CA 签名的证书。
- 如果校验成功,则客户端发送其自身的证书到服务器端。
- 服务器端也通过 CA 验证客户端证书。
- 验证成功之后,服务器端就允许客户端访问受保护的数据了。
为了在示例中启用 mTLS,我们要了解如何处理客户端和服务器端的证书问题。首先需要创建一个具有自签名证书的 CA,还需为客户端和服务器端创建证书签名请求,并且需要使用我们的 CA 对它们进行签名。
与单向安全连接的示例一样,可以使用 OpenSSL 工具生成密钥和证书。
假设我们已经具有了启用客户端–服务器端 mTLS 通信所需的全部证书。如果正确生成了它们,那么在工作空间中会创建以下密钥和证书。
server.key
服务器端的 RSA 私钥。server.crt
服务器端的公开证书。client.key
客户端的 RSA 私钥。client.crt
客户端的公开证书。ca.crt
CA 的公开证书,用来签名所有的公开证书。
我们首先修改示例中的服务器端代码,以便于直接创建 X.509 密钥对,并基于 CA 公钥创建证书池。
01. 在 gRPC 服务器端启用 mTLS
要为 Go 服务器启用 mTLS,需要更新服务器实现的主函数,如代码清单 6-3 所示。
代码清单 6-3 用 Go 语言编写的用于托管 ProductInfo 服务的安全 gRPC 服务器实现
package main
import (
"crypto/tls"
"crypto/x509"
"errors"
pb "productinfo/server/ecommerce"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"io/ioutil"
"log"
"net"
)
var (
port = ":50051"
crtFile = "server.crt"
keyFile = "server.key"
caFile = "ca.crt"
)
func main() {
certificate, err := tls.LoadX509KeyPair(crtFile, keyFile) ➊
if err != nil {
log.Fatalf("failed to load key pair: %s", err)
}
certPool := x509.NewCertPool() ➋
ca, err := ioutil.ReadFile(caFile)
if err != nil {
log.Fatalf("could not read ca certificate: %s", err)
}
if ok := certPool.AppendCertsFromPEM(ca); !ok { ➌
log.Fatalf("failed to append ca certificate")
}
opts := []grpc.ServerOption{
// 为所有传入的连接启用TLS
grpc.Creds( ➍
credentials.NewTLS(&tls.Config {
ClientAuth: tls.RequireAndVerifyClientCert,
Certificates: []tls.Certificate{certificate},
ClientCAs: certPool,
},
)),
}
s := grpc.NewServer(opts...) ➎
pb.RegisterProductInfoServer(s, &server{}) ➏
lis, err := net.Listen("tcp", port) ➐
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
if err := s.Serve(lis); err != nil { ➑
log.Fatalf("failed to serve: %v", err)
}
}
- ❶ 通过服务器端的证书和密钥直接创建 X.509 密钥对。
- ❷ 通过 CA 创建证书池。
- ❸ 将来自 CA 的客户端证书附加到证书池中。
- ❹ 通过创建 TLS 凭证为所有传入的连接启用 TLS。
- ❺ 通过传入的 TLS 服务器凭证创建新的 gRPC 服务器实例。
- ❻ 通过调用生成的 API 将 gRPC 服务注册到新创建的 gRPC 服务器上。
- ❼ 在端口 50051 上创建 TCP 监听器。
- ❽ 绑定 gRPC 服务器到监听器,并开始在端口 50051 上监听传入的消息。
我们已经修改了服务器端,让它只接受已验证客户端的请求。接下来修改客户端代码,使其能够和服务器“交流”。
02. 在 gRPC 客户端启用 mTLS
为了让客户端能够进行连接,客户端代码需要遵循和服务器端代码类似的步骤。可以修改 Go 客户端代码,如代码清单 6-4 所示。
代码清单 6-4 用 Go 语言编写的安全的 gRPC 客户端应用程序
package main
import (
"crypto/tls"
"crypto/x509"
"io/ioutil"
"log"
pb "productinfo/server/ecommerce"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
var (
address = "localhost:50051"
hostname = "localhost"
crtFile = "client.crt"
keyFile = "client.key"
caFile = "ca.crt"
)
func main() {
certificate, err := tls.LoadX509KeyPair(crtFile, keyFile) ➊
if err != nil {
log.Fatalf("could not load client key pair: %s", err)
}
certPool := x509.NewCertPool() ➋
ca, err := ioutil.ReadFile(caFile)
if err != nil {
log.Fatalf("could not read ca certificate: %s", err)
}
if ok := certPool.AppendCertsFromPEM(ca); !ok { ➌
log.Fatalf("failed to append ca certs")
}
opts := []grpc.DialOption{
grpc.WithTransportCredentials( credentials.NewTLS(&tls.Config{ ➍
ServerName: hostname, // 注意,这是必需的!
Certificates: []tls.Certificate{certificate},
RootCAs: certPool,
})),
}
conn, err := grpc.Dial(address, opts...) ➎
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close() ➐
c := pb.NewProductInfoClient(conn) ➏
... // 省略了RPC方法调用
}
- ❶ 通过服务器端的证书和密钥直接创建 X.509 密钥对。
- ❷ 通过 CA 创建证书池。
- ❸ 将来自 CA 的客户端证书附加到证书池中。
- ❹ 添加传输凭证作为连接选项。这里,ServerName 必须与证书中的 Common Name 一致。
- ❺ 传入连接选项,搭建到服务器的安全连接。
- ❻ 传入连接并创建存根。该存根实例包含调用服务器的所有远程方法。
- ❼ 所有事情完成后关闭连接。
现在,我们使用单向 TLS 和 mTLS 搭建了 gRPC 应用程序客户端和服务器端的安全通信通道。下一步是在每次调用的时候启用认证,这意味着凭证信息要附加到调用上。每次客户端调用都带有认证凭证,服务器端检查调用的凭证,并决定是允许还是拒绝客户端的调用。