Go言語のgRPCを爆速で暗号通信(TLS/mTLS)対応する方法

# はじめに

Go言語gRPCを爆速で試す方法

https://qiita.com/totoaoao/items/6bf533b6d2164b74ac09

で紹介されたgRPCのサーバーとクラインアントのプログラムを暗号通信にする方法の紹介です。

# gRPCの暗号通信

gRPCの暗号通信は

https://github.com/grpc/grpc-go/blob/master/examples/features/encryption/README.md

に説明があります。３種類紹介されています。

- TLS
- ALTS
- mTLS(相互TLS)

です。ALTSはGoogle's Application Layer Transport Securityで、Gooleのプラットフォーム(GCP)
でしか使えません。
なので、ここでは、TLSとmTLSに対応します。
TLSは、クライアントがサーバーの証明証を検証して、サーバーを信頼しますが、サーバークライアントを信用しません。WebブラウザーがhttpsでWebサーバーにアクセスする時のほとんどの場合がこれです。
mTLSは、相互に証明書を検証するので、お互いに信用します。


# 証明書を作るプログラム

TLSやmTLSに対応するには、x509の証明書が必要です。gRPCの解説などでは、opensslを使って証明書を作成する方法が紹介されています。手順どおりにやれば、難しくはないのですが、面倒なので、Go言語のプログラムを使って一発で作成します。[Go言語gRPCを爆速で試す方法](https://qiita.com/totoaoao/items/6bf533b6d2164b74ac09)
で作成した`example`のフォルダーの中に、gencertというフォルダーを作成して、このプログラムを作ります。

```terminal
$cd example
$mkdir gencert
```

```go:gencert/main.go
package main

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/pem"
	"log"
	"math/big"
	"net"
	"os"
	"time"
)

func main() {
	cn := "test"
	if len(os.Args) > 1 {
		cn = os.Args[1]
	}
	GenServerCert("../server/server.crt", "../server/server.key")
	GenClientCert("../client/client.crt", "../client/client.key", cn)
}

// genPrivateKey : Generate RSA Key
func genPrivateKey(bits int) (string, *rsa.PrivateKey, error) {
	// Generate the key of length bits
	key, err := rsa.GenerateKey(rand.Reader, bits)
	if err != nil {
		return "", nil, err
	}
	// Convert it to pem
	block := &pem.Block{
		Type:  "RSA PRIVATE KEY",
		Bytes: x509.MarshalPKCS1PrivateKey(key),
	}
	return string(pem.EncodeToMemory(block)), key, nil
}

func getMyIPs() []net.IP {
	ret := []net.IP{}
	ifs, err := net.Interfaces()
	if err != nil {
		log.Printf("get my ips err=%v", err)
		return ret
	}
	for _, i := range ifs {
		if (i.Flags & net.FlagUp) != net.FlagUp {
			continue
		}
		addrs, err := i.Addrs()
		if err != nil {
			continue
		}
		for _, a := range addrs {
			cidr := a.String()
			ip, _, err := net.ParseCIDR(cidr)
			if err != nil {
				continue
			}
			if ip.To4() == nil {
				continue
			}
			ret = append(ret, ip)
		}
	}
	return ret
}

func GenServerCert(cert, key string) {
	kPem, keyBytes, err := genPrivateKey(4096)
	if err != nil {
		log.Fatalf("gen  cert err=%v", err)
	}
	host, err := os.Hostname()
	if err != nil {
		log.Printf("gen server cert err=%v", err)
		host = "localhost"
	}
	subject := pkix.Name{
		CommonName: host,
	}
	notBefore := time.Now()
	notAfter := notBefore.Add(365 * 24 * time.Hour)

	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
	if err != nil {
		log.Fatalf("gen cert err=%v", err)
	}
	template := x509.Certificate{
		SerialNumber:          serialNumber,
		Subject:               subject,
		NotBefore:             notBefore,
		NotAfter:              notAfter,
		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
		BasicConstraintsValid: true,
		IsCA:                  true,
	}
	template.DNSNames = append(template.DNSNames, host)
	if host != "localhost" {
		template.DNSNames = append(template.DNSNames, "localhost")
	}
	template.IPAddresses = getMyIPs()
	derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &keyBytes.PublicKey, keyBytes)
	if err != nil {
		log.Fatalf("gen cert err=%v", err)
	}
	c := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
	if err := os.WriteFile(cert, []byte(c), 0600); err != nil {
		log.Fatalf("gen cert err=%v", err)

	}
	if err := os.WriteFile(key, []byte(kPem), 0600); err != nil {
		log.Fatalf("gen cert err=%v", err)
	}
}

func GenClientCert(cert, key, cn string) {
	kPem, keyBytes, err := genPrivateKey(4096)
	if err != nil {
		log.Fatalf("gen  cert err=%v", err)
	}
	if cn == "" {
		cn = "twlogeye"
	}
	subject := pkix.Name{
		CommonName: cn,
	}
	notBefore := time.Now()
	notAfter := notBefore.Add(365 * 24 * time.Hour)

	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
	if err != nil {
		log.Fatalf("gen client cert err=%v", err)
	}
	template := x509.Certificate{
		SerialNumber:          serialNumber,
		Subject:               subject,
		NotBefore:             notBefore,
		NotAfter:              notAfter,
		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
		BasicConstraintsValid: true,
		IsCA:                  true,
	}
	derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &keyBytes.PublicKey, keyBytes)
	if err != nil {
		log.Fatalf("gen client cert err=%v", err)
	}
	c := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
	if err := os.WriteFile(cert, []byte(c), 0600); err != nil {
		log.Fatalf("gen client cert err=%v", err)

	}
	if err := os.WriteFile(key, []byte(kPem), 0600); err != nil {
		log.Fatalf("gen client cert err=%v", err)
	}
}

```

```terminal
$cd gencert
$go run main.go
```
のように実行されば、サーバー用とクライアント用の証明書が作成されます。
作成される証明書は、自己署名の証明書（いわゆるオレオレ証明書）でCA証明書としても使えます。
サーバー用の証明書が、クライアントにとってのCA証明書です。
逆にクライアント用の証明書がサーバーにとってのCA証明書です。
ややこしいですが、証明書の検証はできます。


# サーバーのプログラムの変更

[Go言語gRPCを爆速で試す方法](https://qiita.com/totoaoao/items/6bf533b6d2164b74ac09)の
サーバーのプログラムを

```diff_go
package main

import (
+	"crypto/tls"
+	"crypto/x509"
+	"log"
	"net"
	"os"

	"github.com/example/grpc_sample"

	"golang.org/x/net/context"
	"google.golang.org/grpc"
+	"google.golang.org/grpc/credentials"
)

func main() {
	log.Print("main start")

	// 9000番ポートでクライアントからのリクエストを受け付けるようにする
	listen, err := net.Listen("tcp", ":9000")
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}
+	var grpcServer *grpc.Server
+	if len(os.Args) < 2 {
+		// not TLS
+		log.Println("not TLS server")
+		grpcServer = grpc.NewServer()
+	} else if os.Args[1] == "mTLS" {
+		// mTLS
+		log.Println("mTLS server")
+		cert, err := tls.LoadX509KeyPair("./server.crt", "./server.key")
+		if err != nil {
+			log.Fatalf("failed to load key pair  err=%v", err)
+		}
+		ca := x509.NewCertPool()
+		caBytes, err := os.ReadFile("../client/client.crt")
+		if err != nil {
+			log.Fatalf("failed to read ca cert  err=%v", err)
+		}
+		if ok := ca.AppendCertsFromPEM(caBytes); !ok {
+			log.Fatalln("failed to parse client.crt")
+		}
+		tlsConfig := &tls.Config{
+			ClientAuth:   tls.RequireAndVerifyClientCert,
+			Certificates: []tls.Certificate{cert},
+			ClientCAs:    ca,
+		}
+		grpcServer = grpc.NewServer(grpc.Creds(credentials.NewTLS(tlsConfig)))
+	} else {
+		// TLS
+		log.Println("TLS server")
+		creds, err := credentials.NewServerTLSFromFile("./server.crt", "./server.key")
+		if err != nil {
+			log.Fatalf("failed to create credentials err=%v", err)
+		}
+		grpcServer = grpc.NewServer(grpc.Creds(creds))
+	}

-	grpcServer := grpc.NewServer()

	// Sample構造体のアドレスを渡すことで、クライアントからGetDataリクエストされると
	// GetDataメソッドが呼ばれるようになる
	grpc_sample.RegisterSampleServiceServer(grpcServer, &Sample{})

	// 以下でリッスンし続ける
	if err := grpcServer.Serve(listen); err != nil {
		log.Fatalf("failed to serve: %s", err)
	}

	log.Print("main end")
}

type Sample struct {
	name string
}

func (s *Sample) GetData(
	ctx context.Context,
	message *grpc_sample.Message,
) (*grpc_sample.Message, error) {
	log.Print(message.Body)
	return &grpc_sample.Message{Body: "レスポンスデータ"}, nil
}

```

のように変更します。

引数がない時は、暗号なし、mTLSという引数をつければ、mTLS、　TLS（なんでもよい）という引数あれば、TLSモードです。

# クライアントのプログラムの変更

[Go言語gRPCを爆速で試す方法](https://qiita.com/totoaoao/items/6bf533b6d2164b74ac09)の
クライアントのプログラムを

```diff_go

package main

import (
+	"crypto/tls"
+	"crypto/x509"
	"log"
	"os"

	"github.com/example/grpc_sample"
	"golang.org/x/net/context"
	"google.golang.org/grpc"
+	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/credentials/insecure"
)

func main() {
	var conn *grpc.ClientConn
	var err error
+	if len(os.Args) < 2 {
+		// not TLS
+		conn, err = grpc.NewClient(
+			":9000",
+			grpc.WithTransportCredentials(insecure.NewCredentials()),
+		)
+	} else if os.Args[1] == "mTLS" {
+		// mTLS
+		cert, err := tls.LoadX509KeyPair("./client.crt", "./client.key")
+		if err != nil {
+			log.Fatalf("failed to load client cert: %v", err)
+		}
+		ca := x509.NewCertPool()
+		caBytes, err := os.ReadFile("../server/server.crt")
+		if err != nil {
+			log.Fatalf("failed to read ca cert  err=%v", err)
+		}
+		if ok := ca.AppendCertsFromPEM(caBytes); !ok {
+			log.Fatalln("failed to parse server.crt")
+		}
+		tlsConfig := &tls.Config{
+			ServerName:   "",
+			Certificates: []tls.Certificate{cert},
+			RootCAs:      ca,
+		}
+		conn, err = grpc.NewClient(":9000", grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)))
+		if err != nil {
+			log.Fatalf("failed to connect  err=%v", err)
+		}
+	} else {
+		// TLS
+		creds, err := credentials.NewClientTLSFromFile("../server/server.crt", "")
+		if err != nil {
+			log.Fatalf("failed to load credentials: %v", err)
+		}
+		conn, err = grpc.NewClient(":9000", grpc.WithTransportCredentials(creds))
+		if err != nil {
+			log.Fatalf("did not connect: %v", err)
+		}
+	}

-   conn, err := grpc.Dial(":9000", grpc.WithInsecure())
	if err != nil {
		log.Fatalf("did not connect: %s", err)
	}
	c := grpc_sample.NewSampleServiceClient(conn)

	response, err := c.GetData(context.Background(), &grpc_sample.Message{Body: "送信データ"})
	if err != nil {
		log.Fatalf("Error when calling SayHello: %s", err)
	}
	log.Print(response.Body)

	defer conn.Close()
}

```
のように変更します。


# 動作確認

|Server/Client|暗号なし|TLS|mTLS|
|---|---|---|---|
|暗号なし| ◯| ✗|✗|
|TLS| ✗| ◯|◯|
|mTLS| ✗| ✗|◯|

のようになるはずです。
サーバーをmTLSでクライアントをmTLS/TLSで試すと

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3874862/81c477e0-a2e6-03e0-8855-1614146e9f02.png)

のようなエラーになります。
TLSの時は、証明書が必要というエラーです。


# 余談

この記事は、

https://note.com/twsnmp/n/n6f4afeb15c98

の開発中にやったことの技術的なまとめです。