gRPC 使用自签名证书开启 TLS 加密

Published: 2022-11-24

Tags: Golang gRPC TLS

本文总阅读量

之前想通过 wireshark 抓包 gRPC 了解其基本通信的传输,整理的过程中看到有介绍 wireshark 解析 TLS 加密的 gRPC 通信的文章,想着可以一并学习,所以本篇先将 gRPC 通信使用自签名证书进行加密。

创建自签名证书

需要注意的是,本流程仅应用于测试,参数和安全性未经验证,不能用于生产环境。

想生成服务端和客户端证书,需要先生成一个证书颁发机构(Certificate Authority, CA),它将验证证书中所有者的身份。

1)生成 CA

$ openssl req -x509 -sha256 -newkey rsa:4096 -nodes -days 365 -keyout ca-key.pem -out ca-cert.pem -subj "/C=TR/ST=ASIA/L=ISTANBUL/O=DEV/OU=TUTORIAL/CN=serve.yasking.org/emailAddress=kissbug8720@gmail.com"
  • 此处的 -sha256 需要注意,如果不指定默认是 SHA1,在 Golang gRPC 测试的时候有如下报错。

2022/11/23 21:33:57 could not greet: rpc error: code = Unavailable desc = connection error: desc = "transport: authentication handshake failed: x509: certificate signed by unknown authority (possibly because of \"x509: cannot verify signature: insecure algorithm SHA1-RSA (temporarily override with GODEBUG=x509sha1=1)\" while trying to verify candidate authority certificate \"serve.yasking.org\")" exit status 1

通过 openssl x509 -noout -text -in client-cert.pem 命令能查看证书信息,可以看到 Signature Algorithm: sha256WithRSAEncryption 字样。

2)生成服务端私钥和 CSR (Certificate Signing Request)

$ openssl req -sha256 -newkey rsa:4096 -nodes -keyout server-key.pem -out server-req.pem -subj "/C=TR/ST=ASIA/L=ISTANBUL/O=DEV/OU=BLOG/CN=serve.yasking.org/emailAddress=info@kissbug8720@gmail.com"

3)签署服务端证书

$ echo "subjectAltName=DNS:serve.yasking.org,IP:0.0.0.0" > server-ext.conf

$ openssl x509 -sha256 -req -in server-req.pem -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem -extfile server-ext.conf

4)生成客户端私钥和 CSR (Certificate Signing Request)

$ openssl req -sha256 -newkey rsa:4096 -nodes -keyout client-key.pem -out client-req.pem -subj "/C=TR/ST=EUROPE/L=ISTANBUL/O=DEV/OU=CLIENT/CN=serve.yasking.org/emailAddress=kissbug8720@gmail.com"

5)签署客户端证书

$ echo "subjectAltName=DNS:serve.yasking.org,IP:0.0.0.0" > client-ext.conf

$ openssl x509 -sha256 -req -in client-req.pem -days 60 -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out client-cert.pem -extfile client-ext.conf

在 Golang gRPC 中使用证书

基于 grpc 的 example/helloworld 示例改造

server.go

package main

import (
    "context"
    "flag"
    "fmt"
    "log"
    "net"

    "crypto/tls"
    "crypto/x509"
    "io/ioutil"

    "google.golang.org/grpc"
    pb "google.golang.org/grpc/examples/helloworld/helloworld"
    "google.golang.org/grpc/credentials"
)

var (
    port = flag.Int("port", 50051, "The server port")
)

// server is used to implement helloworld.GreeterServer.
type server struct {
    pb.UnimplementedGreeterServer
}

// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    log.Printf("Received: %v", in.GetName())
    return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}

func main() {
    flag.Parse()
    lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }

    // read ca's cert, verify to client's certificate
    caPem, err := ioutil.ReadFile("cert/ca-cert.pem")
    if err != nil {
        log.Fatal(err)
    }

    // create cert pool and append ca's cert
    certPool := x509.NewCertPool()
    if !certPool.AppendCertsFromPEM(caPem) {
        log.Fatal(err)
    }

    // read server cert & key
    serverCert, err := tls.LoadX509KeyPair("cert/server-cert.pem", "cert/server-key.pem")
    if err != nil {
        log.Fatal(err)
    }

    // configuration of the certificate what we want to
    conf := &tls.Config{
        Certificates: []tls.Certificate{serverCert},
        ClientAuth:   tls.RequireAndVerifyClientCert,
        ClientCAs:    certPool,
    }

    tlsCredentials := credentials.NewTLS(conf)

    s := grpc.NewServer(grpc.Creds(tlsCredentials))
    pb.RegisterGreeterServer(s, &server{})
    log.Printf("server listening at %v", lis.Addr())
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

client.go

package main

import (
    "context"
    "flag"
    "log"
    "time"
    "fmt"
    "os"

    "crypto/tls"
    "crypto/x509"
    "io/ioutil"
    "google.golang.org/grpc/credentials"

    "encoding/hex"
    "google.golang.org/protobuf/proto"
    "google.golang.org/grpc"
    pb "google.golang.org/grpc/examples/helloworld/helloworld"
)

const (
    defaultName = "world"
)

var (
    addr = flag.String("addr", "serve.yasking.org:50051", "the address to connect to")
    name = flag.String("name", defaultName, "Name to greet")
)

func main() {

    // read ca's cert
    caCert, err := ioutil.ReadFile("cert/ca-cert.pem")
    if err != nil {
        log.Fatal(caCert)
    }

    // create cert pool and append ca's cert
    certPool := x509.NewCertPool()
    if ok := certPool.AppendCertsFromPEM(caCert); !ok {
        log.Fatal(err)
    }

    //read client cert
    clientCert, err := tls.LoadX509KeyPair("cert/client-cert.pem", "cert/client-key.pem")
    if err != nil {
        log.Fatal(err)
    }

    config := &tls.Config{
        Certificates: []tls.Certificate{clientCert},
        RootCAs:      certPool,
    }

    tlsCredential := credentials.NewTLS(config)

    flag.Parse()
    // Set up a connection to the server.
    conn, err := grpc.Dial(*addr,
        grpc.WithTransportCredentials(tlsCredential),
        grpc.WithBlock(),
    )
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
    c := pb.NewGreeterClient(conn)

    // Contact the server and print out its response.
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()

    p := &pb.HelloRequest{Name: *name}

    b, err := proto.Marshal(p)
    if err != nil {
        log.Fatal("proto marshal failed!")
    }
    fmt.Println(hex.EncodeToString(b))

    r, err := c.SayHello(ctx, p)
    if err != nil {
        log.Fatalf("could not greet: %v", err)
    }
    log.Printf("Greeting: %s", r.GetMessage())
}

TLS 抓包截图

可以看到请求已显示为 TLS 加密,至于通信协议显示的是 TLSv1.3,详情内容显示 TLS 1.2,目前还没有精力深入研究。

至此,我们了解到如何生成自签名证书,并在 gRPC 通信中使用。

参考