传输层安全协议(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 连接的主要目的是,控制能够连接服务器端的客户端。与单向安全连接不同,这种方式会将服务器配置为仅接受来自一组范围有限、已验证的客户端的连接。在这种方式中,双方彼此共享公开证书,并校验对方的身份。连接的基本流程如下所示。

  1. 客户端发送一个请求,试图访问服务器端受保护的信息。
  2. 服务器端发送它的 X.509 证书给客户端。
  3. 客户端通过 CA 对接收到的证书进行校验,判断是否为 CA 签名的证书。
  4. 如果校验成功,则客户端发送其自身的证书到服务器端。
  5. 服务器端也通过 CA 验证客户端证书。
  6. 验证成功之后,服务器端就允许客户端访问受保护的数据了。

为了在示例中启用 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 应用程序客户端和服务器端的安全通信通道。下一步是在每次调用的时候启用认证,这意味着凭证信息要附加到调用上。每次客户端调用都带有认证凭证,服务器端检查调用的凭证,并决定是允许还是拒绝客户端的调用。

文档更新时间: 2023-09-02 06:34   作者:Minho