0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

はじめに

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

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

gRPCの暗号通信

gRPCの暗号通信は

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

  • 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を爆速で試す方法
で作成したexampleのフォルダーの中に、gencertというフォルダーを作成して、このプログラムを作ります。

$cd example
$mkdir gencert
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)
	}
}

$cd gencert
$go run main.go

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

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

Go言語gRPCを爆速で試す方法
サーバーのプログラムを

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を爆速で試す方法
クライアントのプログラムを


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

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

余談

この記事は、

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?