理系学生日記

おまえはいつまで学生気分なのか

gRPC で golang server/node client を SSL に対応させる

今回は、gRPC で SSL 通信を行おうと思います。 環境としては、サーバは golang、クライアントは node.js という構成です。

ルート認証局、証明書を作成する。

SSL 通信のため、まずはルート認証局を作成します。 今回は mkcert を使いました。

$ mkcert -install
Created a new local CA at "/Users/kiririmode/Library/Application Support/mkcert" 💥
Password:
The local CA is now installed in the system trust store! ⚡️
Warning: "certutil" is not available, so the CA can't be automatically installed in Firefox! ⚠️
Install "certutil" with "brew install nss" and re-run "mkcert -install" 👈

この段階で、KeyChain にルート認証局の証明書が登録されます。早い! f:id:kiririmode:20190519020348p:plain

次に、証明書を作ります。 例えば kiririmode.com という名前が定義された証明書を作るには以下のようにすれば良いです。

$ mkcert kiririmode.com
Using the local CA at "/Users/kiririmode/Library/Application Support/mkcert"Warning: the local CA is not installed in the Firefox trust store! ⚠️
Run "mkcert -install" to avoid verification errors ‼️

Created a new certificate valid for the following names 📜
 - "kiririmode.com"

The certificate is at "./kiririmode.com.pem" and the key at "./kiririmode.com-key.pem"

こうすると、カレントディレクトリに鍵と証明書のファイルが作成されます。

golang の gRPC サーバに組み込む

golang の gRPC サーバを SSL 対応にするのは簡単で、鍵と証明書ファイルを NewServerTLSFromFileに渡して `TransportCredentials を作成、それを grpc.NewServer に渡すだけで良い。

diff --git a/server/main.go b/server/main.go
index a707402..d868a84 100644
--- a/server/main.go
+++ b/server/main.go
@@ -2,6 +2,7 @@
 package main

 import (
+       "google.golang.org/grpc/credentials"
        "google.golang.org/grpc/keepalive"
        "context"
        "log"
@@ -85,7 +86,16 @@ func main() {
        if err != nil {
                log.Fatalf("failed to listen: %v", err)
        }
+
+       creds, err := credentials.NewServerTLSFromFile(
+               "./kiririmode.com.pem",
+               "./kiririmode.com-key.pem",
+       )
+       if err != nil {
+               log.Fatalf("failed to load certificate: %v", err)
+       }
        s := grpc.NewServer(
+               grpc.Creds(creds),
                grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
                        MinTime: 2 * time.Second,
                        PermitWithoutStream: true,

以前に紹介した grpcurl は TLS にも対応しています。以下のように、-plaintext 無しで呼び出して、きちんと応答が返却されれば成功です。

$ grpcurl -import-path protobuf -proto greeter.proto -d '{ "name": "kiririmode" }' kiririmode.com:50050 Greeter.SayHello
{
  "timestamp": "2019-05-18T15:14:09.134940Z",
  "message": "Hello kiririmode"
}

その他の grpcurl の使い方については、こちらをご参照ください。

Node.js client を SSL に対応させる

Node.js で grpc モジュールを使った場合、 grpc.Client の第2引数 credentials が SSL の肝になります。 今回作成したルート証明書を引数にして以下の様に実装すれば良いですが、実はこれは筋が悪いです。

    const client = new greeter(
        "kiririmode.com:50050",
        grpc.credentials.createSsl(
            fs.readFileSync(
                "/Users/kiririmode//Library/Application Support/mkcert/rootCA.pem"
            )
        ),
        // 以下は keepalive 用設定なので、今回のエントリとは無関係 */
        {
            "grpc.keepalive_time_ms": 1000,
            "grpc.keepalive_timeout_ms": 2000,
            "grpc.http2.min_time_between_pings_ms": 3000,
            "grpc.http2.max_pings_without_data": 0
        }
    )

筋が悪いのは、ルート認証局の証明書をわざわざ渡しているところ。

通常ルート認証局として keychain 上で「信頼する」ように設定したものは、Node.js でも信頼してほしいものなのですが、Node だと「どのルート認証局を信じるのか」はハードコードされているらしい。このため、追加で自己認証局を立てた場合は、こうやって認証局の証明書を渡してやるしかない。。

と思っていたら、Node.js には NODE_EXTRA_CA_CERTS という環境変数で、追加で信頼するルート認証局の証明書を設定できることに気づきました。 これを利用すると、 createSSL の引数は無指定にすることが可能です。

    const client = new greeter(
        "kiririmode.com:50050",
        grpc.credentials.createSsl(),
        /* 以下は keep-alive 用の設定なので、今回のエントリとは無関係 */
        {
            "grpc.keepalive_time_ms": 1000,
            "grpc.keepalive_timeout_ms": 2000,
            "grpc.http2.min_time_between_pings_ms": 3000,
            "grpc.http2.max_pings_without_data": 0
        }
    )
    client.SayHello({ name: "kiririmode" }, (err, res) => {
        console.log(res)
    })

実行するときは、以下のように環境変数を設定して実行すれば良いでしょう。

$ NODE_EXTRA_CA_CERTS=~/Library/Application\ Support/mkcert/rootCA.pem npx babel-node index.js
{
  timestamp: { seconds: '1558197855', nanos: 985252000 },
  message: 'Hello kiririmode'
}

なお、このような設定が必要なのは、あくまでルート認証局を自分で作ったからで、オレオレ証明書を作らなければ、多くの場合このような指定は不要になるはずです。

node のソース

const path = require("path")
const grpc = require("grpc")
const protoLoader = require("@grpc/proto-loader")
const fs = require("fs")

const PROTO_PATH = path.join(__dirname, "greeter.proto")

try {
    const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
        keepCase: false,
        longs: String,
        enums: String,
        defaults: true,
        oneofs: true
    })
    const greeter = grpc.loadPackageDefinition(packageDefinition).Greeter
    const client = new greeter(
        "kiririmode.com:50050",
        grpc.credentials.createSsl(),
        {
            "grpc.keepalive_time_ms": 1000,
            "grpc.keepalive_timeout_ms": 2000,
            "grpc.http2.min_time_between_pings_ms": 3000,
            "grpc.http2.max_pings_without_data": 0
        }
    )
    client.SayHello({ name: "kiririmode" }, (err, res) => {
        console.log(res)
    })
} catch (ex) {
    console.error(ex)
}