在Golang中使用TLS/SSL证书进行gRPC客户端和服务器通信

665 阅读1分钟

这是我在这里已经有的更新版本。我不得不发布这个版本,只是因为Golang v1.15破坏了处理证书的方式。你会得到下面这个错误:

transport: authentication handshake failed: x509: certificate relies on legacy Common Name field, use SANs or temporarily enable Common Name matching with GODEBUG=x509ignoreCN=0

为了解决这个错误,我们需要改变我们创建证书的方式。在这样做的同时,我不妨从头开始使用一个全新的例子。简而言之,我们的客户和服务器应用程序将使用TLS来相互通信。

结构

├── Makefile
├── cert
│   ├── cert.conf
│   ├── server.crt
│   └── server.key
├── client
│   └── main.go
├── go.mod
├── hello.pb.go
├── hello.proto
├── message
│   ├── client.go
│   └── server.go
└── server
    └── main.go

证书

cert.conf 文件的内容如下所示:

[ req ]
default_bits = 2048
prompt = no
default_md = sha256
req_extensions = req_ext
distinguished_name = dn

[ dn ]
C = UK
ST = London
L = London
O = Inanzzz Ltd.
OU = Information Technologies
emailAddress = email@email.com
CN = localhost

[ req_ext ]
subjectAltName = @alt_names

[ alt_names ]
DNS.1 = localhost
DNS.2 = inanzzz.com
DNS.3 = www.inanzzz.com

让我们来创建私钥和证书:

$ openssl genrsa -out cert/server.key 2048

$ openssl req -nodes -new -x509 -sha256 -days 1825 -config cert/cert.conf -extensions 'req_ext' -key cert/server.key -out cert/server.crt

这里重要的一点是,subjectAltName 属性。Golang v1.15需要SNA,它与这个属性直接相关。让我们确认一下细节。

$ openssl x509 -in cert/server.crt -text
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            59:b3:15:04:02:f9:04:0f:77:b5:0c:c6:dd:d9:65:0f:06:2d:e5:cd
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C = UK, ST = London, L = London, O = Inanzzz Ltd., OU = Information Technologies, emailAddress = email@email.com, CN = localhost
        Validity
            Not Before: Sep 13 19:13:52 2020 GMT
            Not After : Sep 12 19:13:52 2025 GMT
        Subject: C = UK, ST = London, L = London, O = Inanzzz Ltd., OU = Information Technologies, emailAddress = email@email.com, CN = localhost
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (4096 bit)
                Modulus:
                    00:a7:90:4e:47:5b:3a:87:20:55:c8:36:8d:a9:00:
                    ............................................:
                    e9:56:3f
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Subject Alternative Name: 
                DNS:localhost, DNS:inanzzz.com, DNS:www.inanzzz.com
    Signature Algorithm: sha256WithRSAEncryption
         9a:40:93:5a:20:6e:20:9f:14:70:c5:5b:96:ee:da:ac:22:98:
         .....................................................:
         44:fe:3d:31:03:53:b7:e1
-----BEGIN CERTIFICATE-----
MIIGADCCA+igAwIBAgIUWbMVBAL5BA93tQzG3dllDwYt5c0wDQYJKoZIhvcNAQEL
................................................................
mai+5Eck8n8uEJzQnIB4oDDb4eucoL26GLDa4YUGONZdeijOcpluTgXt/uNE/j0x
A1O34Q==
-----END CERTIFICATE-----

如果你没有使用subjectAltName 属性,你上面的输出就不会包含X509v3 Subject Alternative Name 的扩展。

文件

运行make compile 命令来生成hello.pb.go 文件。

制作文件

.PHONY: compile
compile:
	protoc --go_out=plugins=grpc:. --go_opt=paths=source_relative hello.proto

.PHONY: client
client:
	go run --race client/main.go

.PHONY: server
server:
	go run --race server/main.go

hello.proto

syntax = "proto3";

package message;

option go_package = "github.com/inanzzz/hello";

message MessageRequest {
    string text = 1;
}

message MessageResponse {
    bool ok = 1;
}

service MessageService {
    rpc SendMessage (MessageRequest) returns (MessageResponse) {}
}

go.mod

module github.com/inanzzz/hello

go 1.15

require (
	github.com/golang/protobuf v1.4.2
	golang.org/x/net v0.0.0-20200904194848-62affa334b73 // indirect
	golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f // indirect
	golang.org/x/text v0.3.3 // indirect
	google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d // indirect
	google.golang.org/grpc v1.31.1
	google.golang.org/protobuf v1.25.0
)

server/main.go

package main

import (
	"log"
	"net"

	"github.com/inanzzz/hello"
	"github.com/inanzzz/hello/message"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
)

func main() {
	log.Println("server")

	listener, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatalln(err)
	}

	creds, err := credentials.NewServerTLSFromFile("cert/server.crt", "cert/server.key")
	if err != nil {
		log.Fatalln(err)
	}

	grpcServer := grpc.NewServer(grpc.Creds(creds))

	messageServer := message.NewServer()

	client.RegisterMessageServiceServer(grpcServer, messageServer)

	log.Fatalln(grpcServer.Serve(listener))
}

message/server.go

package message

import (
	"context"
	"log"

	"github.com/inanzzz/hello"
)

type Server struct {
	client.UnimplementedMessageServiceServer
}

func NewServer() Server {
	return Server{}
}

func (s Server) SendMessage(ctx context.Context, req *client.MessageRequest) (*client.MessageResponse, error) {
	log.Println(req)

	return &client.MessageResponse{Ok:true}, nil
}

client/main.go

这是版本1:

package main

import (
	"context"
	"log"
	"time"

	"github.com/inanzzz/hello/message"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
)

func main() {
	log.Println("client")

	creds, err := credentials.NewClientTLSFromFile("cert/server.crt", "localhost")
	if err != nil {
		log.Fatalln(err)
	}

	opts := []grpc.DialOption{
		grpc.WithTransportCredentials(creds),
	}

	conn, err := grpc.Dial(":50051", opts...)
	if err != nil {
		log.Fatalln(err)
	}
	defer conn.Close()

	messageClient := message.NewClient(conn, time.Second)
	res, err := messageClient.SendMessage(context.Background(), "Hello")

	log.Println("RES:", res)
	log.Println("ERR:", err)
}

这是版本2:

package main

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"io/ioutil"
	"log"
	"time"

	"github.com/inanzzz/hello/message"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
)

func main() {
	log.Println("client")

	caCert, err := ioutil.ReadFile("cert/server.crt")
	if err != nil {
		log.Fatalln(err)
	}

	rootCAs := x509.NewCertPool()
	rootCAs.AppendCertsFromPEM(caCert)

	tlsConf := &tls.Config{
		RootCAs:            rootCAs,
		InsecureSkipVerify: false,
		MinVersion:         tls.VersionTLS12,
		ServerName:         "localhost",
	}

	opts := []grpc.DialOption{
		grpc.WithTransportCredentials(credentials.NewTLS(tlsConf)),
	}

	conn, err := grpc.Dial(":50051", opts...)
	if err != nil {
		log.Fatalln(err)
	}
	defer conn.Close()

	messageClient := message.NewClient(conn, time.Second)
	res, err := messageClient.SendMessage(context.Background(), "Hello")

	log.Println("RES:", res)
	log.Println("ERR:", err)
}

message/client.go

package message

import (
	"context"
	"time"

	"github.com/inanzzz/hello"
	"google.golang.org/grpc"
)

type Client struct {
	messageClient client.MessageServiceClient
	timeout       time.Duration
}

func NewClient(conn grpc.ClientConnInterface, timeout time.Duration) Client {
	return Client{
		messageClient: client.NewMessageServiceClient(conn),
		timeout:       timeout,
	}
}

func (c Client) SendMessage(ctx context.Context, message string) (*client.MessageResponse, error) {
	ctx, cancel := context.WithDeadline(ctx, time.Now().Add(c.timeout))
	defer cancel()

	return c.messageClient.SendMessage(ctx, &client.MessageRequest{Text: message})
}

测试

$ make server
go run --race server/main.go
2020/09/13 20:39:17 server

# This shall output as soon as you run client
2020/09/13 20:39:22 text:"Hello"
$ make client
go run --race client/main.go
2020/09/13 20:39:22 client
2020/09/13 20:39:22 RES: ok:true
2020/09/13 20:39:22 ERR: nil