はじめに
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
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で試すと
のようなエラーになります。
TLSの時は、証明書が必要というエラーです。
余談
この記事は、
の開発中にやったことの技術的なまとめです。