「连载四」TLS 证书认证

    此时存在一个安全问题,先前的例子中 gRPC Client/Server 都是明文传输的,会不会有被窃听的风险呢?

    从结论上来讲,是有的。在明文通讯的情况下,你的请求就是裸奔的,有可能被第三方恶意篡改或者伪造为“非法”的数据

    抓个包

    image

    嗯,明文传输无误。这是有问题的,接下将改造我们的 gRPC,以便于解决这个问题 😤

    证书生成

    自签公钥

    填写信息

    1. Country Name (2 letter code) []:
    2. State or Province Name (full name) []:
    3. Locality Name (eg, city) []:
    4. Organization Name (eg, company) []:
    5. Organizational Unit Name (eg, section) []:
    6. Common Name (eg, fully qualified host name) []:go-grpc-example
    7. Email Address []:

    生成完毕

    生成证书结束后,将证书相关文件放到 conf/ 下,目录结构:

    1. $ tree go-grpc-example
    2. go-grpc-example
    3. ├── client
    4. ├── conf
    5. ├── server.key
    6. └── server.pem
    7. ├── proto
    8. └── server
    9. ├── simple_server
    10. └── stream_server

    在 simple_server 中,为什么“啥事都没干”就能在不需要证书的情况下运行呢?

    1. grpc.NewServer()

    在服务端显然没有传入任何 DialOptions

    Client

    在客户端留意到 grpc.WithInsecure() 方法

    1. func WithInsecure() DialOption {
    2. return newFuncDialOption(func(o *dialOptions) {
    3. o.insecure = true
    4. })
    5. }

    在方法内可以看到 WithInsecure 返回一个 DialOption,并且它最终会通过读取设置的值来禁用安全传输

    那么它“最终”又是在哪里处理的呢,我们把视线移到 grpc.Dial() 方法内

    1. func DialContext(ctx context.Context, target string, opts ...DialOption) (conn *ClientConn, err error) {
    2. ...
    3. for _, opt := range opts {
    4. opt.apply(&cc.dopts)
    5. }
    6. ...
    7. if !cc.dopts.insecure {
    8. if cc.dopts.copts.TransportCredentials == nil {
    9. return nil, errNoTransportSecurity
    10. }
    11. } else {
    12. if cc.dopts.copts.TransportCredentials != nil {
    13. return nil, errCredentialsConflict
    14. for _, cd := range cc.dopts.copts.PerRPCCredentials {
    15. if cd.RequireTransportSecurity() {
    16. return nil, errTransportCredentialsMissing
    17. }
    18. }
    19. }
    20. ...
    21. creds := cc.dopts.copts.TransportCredentials
    22. cc.authority = creds.Info().ServerName
    23. } else if cc.dopts.insecure && cc.dopts.authority != "" {
    24. cc.authority = cc.dopts.authority
    25. } else {
    26. // Use endpoint from "scheme://authority/endpoint" as the default
    27. // authority for ClientConn.
    28. cc.authority = cc.parsedTarget.Endpoint
    29. }
    30. ...
    31. }

    gRPC

    接下来我们将正式开始编码,在 gRPC Client/Server 上实现 TLS 证书认证的支持 🤔

    TLS Server

    1. package main
    2. import (
    3. "context"
    4. "log"
    5. "net"
    6. "google.golang.org/grpc"
    7. "google.golang.org/grpc/credentials"
    8. pb "github.com/EDDYCJY/go-grpc-example/proto"
    9. )
    10. ...
    11. const PORT = "9001"
    12. func main() {
    13. c, err := credentials.NewServerTLSFromFile("../../conf/server.pem", "../../conf/server.key")
    14. if err != nil {
    15. log.Fatalf("credentials.NewServerTLSFromFile err: %v", err)
    16. }
    17. server := grpc.NewServer(grpc.Creds(c))
    18. pb.RegisterSearchServiceServer(server, &SearchService{})
    19. lis, err := net.Listen("tcp", ":"+PORT)
    20. if err != nil {
    21. log.Fatalf("net.Listen err: %v", err)
    22. }
    23. server.Serve(lis)
    24. }
    • credentials.NewServerTLSFromFile:根据服务端输入的证书文件和密钥构造 TLS 凭证
    1. func NewServerTLSFromFile(certFile, keyFile string) (TransportCredentials, error) {
    2. cert, err := tls.LoadX509KeyPair(certFile, keyFile)
    3. if err != nil {
    4. return nil, err
    5. }
    6. }
    • grpc.Creds():返回一个 ServerOption,用于设置服务器连接的凭据。用于 grpc.NewServer(opt ...ServerOption) 为 gRPC Server 设置连接选项
    1. package main
    2. import (
    3. "context"
    4. "log"
    5. "google.golang.org/grpc"
    6. "google.golang.org/grpc/credentials"
    7. pb "github.com/EDDYCJY/go-grpc-example/proto"
    8. )
    9. const PORT = "9001"
    10. func main() {
    11. c, err := credentials.NewClientTLSFromFile("../../conf/server.pem", "go-grpc-example")
    12. if err != nil {
    13. log.Fatalf("credentials.NewClientTLSFromFile err: %v", err)
    14. }
    15. conn, err := grpc.Dial(":"+PORT, grpc.WithTransportCredentials(c))
    16. if err != nil {
    17. log.Fatalf("grpc.Dial err: %v", err)
    18. }
    19. defer conn.Close()
    20. client := pb.NewSearchServiceClient(conn)
    21. resp, err := client.Search(context.Background(), &pb.SearchRequest{
    22. Request: "gRPC",
    23. })
    24. if err != nil {
    25. log.Fatalf("client.Search err: %v", err)
    26. }
    27. log.Printf("resp: %s", resp.GetResponse())
    28. }
    • credentials.NewClientTLSFromFile():根据客户端输入的证书文件和密钥构造 TLS 凭证。serverNameOverride 为服务名称
    1. func NewClientTLSFromFile(certFile, serverNameOverride string) (TransportCredentials, error) {
    2. b, err := ioutil.ReadFile(certFile)
    3. if err != nil {
    4. return nil, err
    5. }
    6. cp := x509.NewCertPool()
    7. if !cp.AppendCertsFromPEM(b) {
    8. return nil, fmt.Errorf("credentials: failed to append certificates")
    9. }
    10. return NewTLS(&tls.Config{ServerName: serverNameOverride, RootCAs: cp}), nil
    11. }
    • grpc.WithTransportCredentials():返回一个配置连接的 DialOption 选项。用于 grpc.Dial(target string, opts ...DialOption) 设置连接选项
    1. func WithTransportCredentials(creds credentials.TransportCredentials) DialOption {
    2. return newFuncDialOption(func(o *dialOptions) {
    3. o.copts.TransportCredentials = creds
    4. })
    5. }

    验证

    请求

    重新启动 server.go 和执行 client.go,得到响应结果

    1. $ go run client.go

    抓个包

    成功。

    在本章节我们实现了 gRPC TLS Client/Servert,你以为大功告成了吗?我不 😤

    问题

    你仔细再看看,Client 是基于 Server 端的证书和服务名称来建立请求的。这样的话,你就需要将 Server 的证书通过各种手段给到 Client 端,否则是无法完成这项任务的

    问题也就来了,你无法保证你的“各种手段”是安全的,毕竟现在的网络环境是很危险的,万一被…

    参考

    「连载四」TLS 证书认证 - 图2