ハトネコエ Web がくしゅうちょう

プログラミングやサーバー関連、チームマネジメントなど得た技術のまとめ

gRPCのクライアント実装でサーバー証明書の検証を入れる方法

このあいだの gRPC の記事の「失敗5」の項で

サーバー側の証明書の正当性を検証するステップはスキップするという方向の実装

にしたのですが、サーバー証明書の検証をおこなう方法がわかったので書いておきます。

なお、今回言う「サーバー証明書」というのは、
自分の運営するドメインに対して取得した X.509証明書 の意味で捉えてください。
よく言う「SSL証明書」ですね。(現在はSSL通信ではなくTLS通信だけど……

方法1: サーバー証明書を用いる

Let's Encrypt での認証後、生成されるサーバー証明書 cert.pem をサーバーから持ってきて、
それを証明書検証に用いることも可能です。
……が、これは Let's Encrypt の仕様上、認証後から3ヶ月で使えなくなってしまうものです。

更新のたびにサーバーから取りに行くのは現実的ではなさそうです。

方法2: ルート証明書を用いる

Let's Encrypt で SSL/TLS 化した場合、それはちゃんと認証局によって認証されたものですから、
ルート証明書 を用いて サーバー証明書の正当性の検証 がおこなえます。

これについて詳しく書きます。
ドメインを認証しサーバー証明書を発行したのが、中間認証局「Let's Encrypt Authority X3」だとします。
その中間認証局認証局として承認したのはルート証明局「DST Root CA X3」です。

ルート証明書というのは、このルート証明局の持つ公開鍵情報を持っています。

クライアントはサーバーから、サーバー証明書の正当性を検証するため、
サーバー証明書や中間証明書を受け取ります。

サーバー証明書や中間証明書は X.509 の形式であり、その中にはその証明書を発行した認証局情報や、
証明書の正当性を検証するための Signature 情報を含みます。

クライアントは、認証局情報を元にその認証局の公開鍵を確認し、
証明書の Signature を公開鍵で復号したものと、証明書自身を使ってハッシュ化した値を比較。
一致するならば、それは正当な証明書と認定されます。

参考: PKI(後編)---X.509証明書とPKIの仕組み(3ページ目) | 日経 xTECH(クロステック)

この計算をおこなうために、ルート認証局の公開鍵情報が必要なのです。

で、実は gRPC のリポジトリにそのルート証明書が置いてあります!

grpc/roots.pem at master · grpc/grpc · GitHub

README によると、https://hg.mozilla.org/mozilla-central/file/tip/security/nss/lib/ckfw/builtins にある certdata.txt から
https://github.com/agl/extract-nss-root-certs を使って作られているので、
個人で生成し直すことも可能そうです。

特に古くなさそうであれば、そのままダウンロードして使ってみましょう。

実装例

ほとんど前回の記事と同じで、

creds := credentials.NewTLS(&tls.Config{InsecureSkipVerify: true})

certFile := build.Default.GOPATH + "/src/google.golang.org/grpc/roots.pem"
creds, err := credentials.NewClientTLSFromFile(certFile, "")

になったくらいの差です。(roots.pem は grpc ディレクトリ以下に入れてみました)

なお、roots.pem の情報を見ると

openssl x509 -text -fingerprint -noout -in roots.pem

# 中略
Validity
    Not Before: Sep  1 12:00:00 1998 GMT
    Not After : Jan 28 12:00:00 2028 GMT

と書いてあるので、
2028年1月28日以降も同様のルート証明書を使い続けることは出来なさそうなので注意です。

おまけ

特定のドメインに関する中間認証局とルート認証局を確認するコマンド

openssl s_client -showcerts -verify 5 -connect yahoo.co.jp:443 < /dev/null

verify depth is 5
CONNECTED(00000003)
depth=2 C = IE, O = Baltimore, OU = CyberTrust, CN = Baltimore CyberTrust Root
verify return:1
depth=1 C = JP, O = "Cybertrust Japan Co., Ltd.", CN = Cybertrust Japan Public CA G3
verify return:1
depth=0 C = JP, ST = Tokyo, L = Chiyoda-ku, O = Yahoo Japan Corporation, OU = EDGE_20190131, CN = *.yahoo.co.jp
verify return:1
---
Certificate chain
 0 s:/C=JP/ST=Tokyo/L=Chiyoda-ku/O=Yahoo Japan Corporation/OU=EDGE_20190131/CN=*.yahoo.co.jp
   i:/C=JP/O=Cybertrust Japan Co., Ltd./CN=Cybertrust Japan Public CA G3

# 以下略

GOPATH 以下にシンボリックリンクを置いて楽できる!

Go 言語での開発のしんどいところ私的 No. 1 は、
ソースを GOPATH 以下のディレクトリで管理しないといけないところです。

alias cdgo='cd $(echo "$GOPATH" | awk -F "[:]" "{print $1}")'

エイリアスを振ったとしても、(ちなみに上は GOPATH を複数定義している場合にも対応できるよう、複雑にしている)
ふだんの開発用ディレクトリとの行き来は多少めんどくさい。

Go の実行を Docker 内でおこなうというのは一つの手段ですが、
テキストエディタは補完が便利な Visual Studio Code を使いたいな〜、となると、
import が失敗して補完が効かないし import エラーは表示されちゃうしで上手くいきません。

横着する方法の一つとして考えたのが、
GOPATH 下にシンボリックリンクを置くことです!

f:id:nekonenene:20190212022606p:plain

「test_project」というフォルダを作って、以下のような Makefile を書いてみました。

local_gopath := $(shell echo "${GOPATH}" | awk -F '[:]' '{print ${1}}')

.PHONY: init
init:
  mkdir -p $(local_gopath)/src/myprojects
  ln -sf $(shell pwd) $(local_gopath)/src/myprojects

make init をおこなうことによって、
この「test_project」ディレクトリへのシンボリックリンクが、
$GOPATH/src/myprojects ディレクトリに置かれます。

では実験してみます。

「test_project」ディレクトリに「hello」フォルダを作り、その中には以下のような main.go を作成します。

package hello

import (
    "fmt"
)

// PrintHello は Hello!! と出力するだけのメソッド
func PrintHello() {
    fmt.Println("Hello!!")
}

保存したら今度は「test_project」ディレクトリに以下のような main.go を作成します。

package main

import (
    "fmt"
    "myprojects/test_project/hello"
)

func main() {
    fmt.Println("It's main.")
    hello.PrintHello()
}

保存しましたが、特に golint に怒られもしていないようです。
そして実行します。

go run main.go

It's main.
Hello!!

無事に Hello!! と出力されましたね。

このように、シンボリックリンクを置くことで、無理やり、GOPATH の呪縛から逃れることが出来ました。
横着しすぎです。

しかしこれも欠点があって、
ライブラリの依存管理をおこなう dep は、「test_project」ディレクトリ下でおこなうことが出来ません。

dep init

init failed: unable to detect the containing GOPATH: /Users/nekonenene/Programs/test_project is not within a known GOPATH/src

まあ、そうだよね、うん……。

結論: 横着しない。Go に入っては Go に従え

最初からわかっていた答えですが、いやぁ、それでも横着したいなぁ・・・w

ufw のログの読み方(SPT, DPT など)

1. ufw とは

ufwUbuntu Firewall の略……ではなく
Uncomplicated Firewall (複雑じゃないファイアウォール)の略です。

「昔は Ubuntu Firewall の略だったんじゃないの?」と邪推したんですが、
2008年1月24日時点の Wiki
『Name: ufw (Uncomplicated Firewall)』と記述してあるので、開発当初からその略みたいです。

iptables の操作が複雑なので、それを簡単に操作するためのラッパーです。

2. ログの場所

/var/log/syslog/var/log/ufw.log の両方に吐き出されます。
syslog は他のシステムログと混じるので、 ufw.log の方を見るのがわかりやすいと思います。

3. ログの読み方

読み方は、ここのサイトがわかりやすかったです。ありがとうございます。

Feb 10 06:30:51 the-host-name kernel: [716156.938348] [UFW BLOCK] 
IN=eth0 OUT= MAC=11:01:01:01:01:01:22:02:02:02:02:02:08:00 
SRC=189.23.45.67 DST=123.45.67.89 
LEN=40 TOS=0x00 PREC=0x00 TTL=244 ID=17582 PROTO=TCP 
SPT=40596 DPT=8088 WINDOW=1024 RES=0x00 SYN URGP=0 

例えばこんなログが出ていました。(読みやすさのため改行を入れてます)

IPのインターフェイス(Internet Protocol Interface)について記載している
RFC791 3.3 Interfaces の項で、
各々が何を指すのかほぼ確認できます。

順番に書いていきます。

3-1. Feb 10 06:30:51

日時ですね。これはそのまんまです。

3-2. the-host-name

そのマシンのホスト名です。 echo $(hostname) で出てくる値と同じだと思います。

3-3. kernel: [716156.938348]

brackets(角カッコ)の中身は、OSが起動してからの秒数を指しています。

716156 / 60 / 60 / 24 = 8.3 程度なので、およそ起動してから8日が経過していることがわかります。

3-4. [UFW BLOCK]

ufw が以下の通信をブロックしたことを示します。

3-5. IN=eth0 OUT=

IN が incoming interface , OUT が outgoing interface を指すようです。
……らしいんですが、ネットワークのことをわかっていなくて、それが具体的になにかわかっていないです。
こちらが参考になるかもしれません: IP マルチキャストのトラブルシューティング - Cisco

この場合は、OUT側が空白なので、 incomig (相手から自分宛てへ)の通信であったことがわかります。

3-6. MAC=11:01:01:01:01:01:22:02:02:02:02:02:08:00

これは先ほどの参考ページで本当に簡潔にまとめてくれているのですが、
先頭の 6 octet (11:01:01:01:01:01)が宛先のMACアドレス
その後の 6 octet (22:02:02:02:02:02)が送信元のMACアドレス

最後の 2 octet がイーサネットタイプを指すそう。(参考: RFC 1340 ETHERNET NUMBERS OF INTEREST
0800 が DOD IP (DoD参照モデルとしてのIP)として参考先には記載がありますね。

3-7. SRC=189.23.45.67 DST=123.45.67.89

SRC が source 、送信元のIPアドレス を、
DST が destination送信先IPアドレス を指します。

3-8. LEN=40

IPパケットの長さです。
IPヘッダとIPペイロード(データ)の合計値です。

IPヘッダについてはこの記事が詳しいです: TCP/IP - IPとは

3-9. TOS=0x00

Type of Service です。
0x00 の場合は CS0 らしいです。

Howto:Calculate Values for Type of Service (ToS) from DiffServ or DSCP Values - innovaphone-wiki

これは DSCP 値としては 000 000 にあたり、
処理の優先レベルがもっとも低く、破棄レベルも低く処理されることを示します。

参考: QoS - DSCP(Differentiated Services Code Point)とは

3-10. PREC=0x00

IP Precedence の値 を指すそう。(「Precedence」は「優先度」を指す英単語らしい…)

0〜7 の 8 段階あり、この例では 0x00 なので、やっぱり優先度は低いです。

参考: QoS - IP Precedence(IPプレシデンス)とは

3-11. TTL=244

おなじみ Time to live です。パケットが破棄されるまでの時間(秒数)を指します。
知ってる風に書いたけど、「パケットが破棄される」という概念を実はよくわかっていない……。

3-12. ID=17582

IPヘッダにおける識別番号(Identifier)です。
ちなみに16bitなので、IDの最大値は 65535 のはず。

3-13. PROTO=TCP

これはそのままプロトコルですね。
TCPプロトコルの通信であることを示しています。

3-14. SPT=40596 DPT=8088

SPT は Source Port、つまり送信元のポート番号を示し、
DPT は Destination Port送信先のポート番号を示します。

つまり相手側は、8088番ポートにアクセスしようとして、 ufw にブロックされたと言えます。

ログを見てみると、やはり 22番ポートへのアクセスが一番多く、
次に 8080番ポートへのアクセス、そして 8088番ポートや 3389番ポートへのアクセスが見られる印象でした。

3-15. WINDOW=1024

TCPヘッダにおけるウィンドウサイズを指します。

ウィンドウ制御についてはこちらを参考に: TCP/UDP - TCPウインドウ制御 - ネットワークエンジニアを目指して

3-16. RES=0x00

RES はTCPヘッダにある予約領域です。
将来的な拡張のために 6 bit 用意されていますが、今のところ使われていないので
常に 0 が入るようです。

3-17. SYN URGP=0

SYN はコントロールフラグにて「SYN(コネクションの確立を要求)」のフラグがONであることを示しています。

URGP は Urgent Pointer の略で、コントロールフラグで「URG(Urgent=緊急)」のフラグがONである場合には、
緊急データの位置を指す役目を持ちますが、そうでなければ常に 0 です。

参考: TCP/IP - TCPとは - TCPヘッダ

4. まとめ

詳細に書きましたが、ファイアウォールの動作ログとしては

の 4 つがわかっていれば、困ることはなさそうですね。

すごく久しぶりに IP と TCP のことを調べて、
「マスタリングTCP/IPのことほとんど覚えていないな……」と反省しました・・・。

マスタリングTCP/IP 入門編 第5版

マスタリングTCP/IP 入門編 第5版

IPヘッダやTCPヘッダに何があるかくらいは、頭に入れておくと楽そうです。

gRPC x Go x nginx での失敗談

この前こんな gRPC の入門記事を書いたわけですよ。

nekonenene.hatenablog.com

だからもう実際の運用も恐くないだろうな〜って思っていたんですけど、
実際にサーバーに置いて動かそうとしたらいろいろ失敗があったので書いておきます。

動かそうとしたコードはこんな感じなので、時間ある方は読んでもらえば。

失敗1: nginx と gRPC サーバー間は平文通信で大丈夫だった

接続としては

クライアント→(443番)→ nginx がプロキシ役 →(localhost:50123)→ Docker(50123:50051) → Go による gRPC サーバー

となっていて、とにかく上手く行かなかったので、
「もしやローカル通信部分も SSL/TLS 化しないと gRPC の場合は行けないのでは?」と、
自己署名証明書作ったり、無理やりサーバー証明を Docker にreadonlyでマウントしたり、けっこう迷走しました。

が、結果としては TLSで問題なかった
次に書くところに気付かなかったのが一番の問題でした。

失敗2: grpc_pass を用いる

いつも通り proxy_pass を用いて、

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name grpc-test.hatone.net;
# (中略)
    location / {
        proxy_pass http://localhost:50123;
    }
}

と nginx の設定を書いていたんですが、こうじゃないんですね!

www.nginx.com

NGINX 1.13.10 で gRPC サポートをしたよ〜!
という、上の記事に書かれていました。
(※ nginx -v で、お使いの nginx のバージョンが v1.13.10 以上かご確認ください)

proxy_pass ではなく grpc_pass 、これが答えだった・・・!

よって、nginx の設定はこうなります。

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name grpc-test.hatone.net;
# (中略)
    location / {
        grpc_pass grpc://localhost:50123;
    }
}

grpc:// 部分はオプションらしいので書かなくて大丈夫らしいんですが、なんとなく不安なので。
ちなみに localhost 部分で SSL/TLS 通信をおこなう場合は grpcs:// とするようです。(参考: IBM Blockchain Platform にアプリケーションをデプロイする

失敗3: grpcurl の使い方に失敗していた

動作確認には前回も紹介した grpcurl を使っていました。

正解から書くと、以下のようなリクエストで行けるんですが、

grpcurl -d '{"name": "Nanako", "age": 35}' grpc-test.hatone.net:443 hello.Greeter/SayHello

ここに行き着くまでかなりの失敗をしていて、
サーバー側の実装が失敗してるのかと勘違いして時間をけっこう無駄にしました。

例えば package name を helloworld から hello に変えたことを忘れて helloworld.Greeter/SayHello でリクエストしてたり、
:443 を付けずにリクエストして『missing port in address』のエラーを見たり。

grpcurl の README でも :443 を付けているんですけど、
「README ではポート番号を付けてなかったはず……」と間違った記憶をなぜか信じていました(笑)

エラーもちゃんと理解していなく、「ポート未指定の場合のデフォルトのポート番号がなにかあって、
それがファイアウォールで通信拒否されてる?」と間違った仮説を持っていました。

ファイアウォールのログを確認した結果、無く、
調べた末、ふつうに golang 側のエラーだと知りました・・・。
https://github.com/golang/go/blob/go1.11.5/src/net/ipsock.go#L157

失敗4: サーバー側の実装に失敗していた期間があった

サーバー側の実装をいろいろいじっているうちに、
ふつうにミスって動いていない状態になっていたことがありました。

他の人のコードを見てたら

lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port))

となってるのあって、「あっ、localhost って付けても動くんだ」と思ったので付けたら
実際は正常動作していなかったという。

lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port))

でないとダメでした。

「いろいろ変えたけど、改めてローカルで動くんだっけ……」と試して問題に気付けました。

失敗5: クライアントの実装も難しかった

grpcurl がどうしてもわからなくて、ちゃんとクライアントを実装しようとやってみたんですけど
けっこう難しかったです。

公式の実装例だとクライアント側も証明書を必要としていて、
「うーん、それは難しい……」と。

今回は実運用はまだですので、サーバー側の証明書の正当性を検証するステップはスキップするという方向の実装にしました。

InsecureSkipVerify: true がそれですね。

実運用のときは証明書の正当性の検証も入れたほうがいいとは思いますが、
Let's Encrypt で定期的に更新されるタイプの証明書の検証って運用負荷低く実現できるのか気になります。

まあ、TLS について理解していない部分が多いので、まずはそこの勉強から頑張らねばですね!

おわりに

上で書いた、nginx の gRPC サポート発表の記事が 2018/03/17 だったり、
gRPC が実運用として活発に使われるようになったのはここ1年だと思うのですが、
そのわりには情報が多く、けっこう助かっています。

でも上のように何度もつまずいて大変でした! 丸2日かかってしまいました。連休でよかった。
Golang 自体にまだ全然慣れていないので、gRPC 特有の型なのか Go 付属ライブラリの型なのかわからなくて、コード読むのに戸惑ったり…(笑)

次は、このサーバーに Android アプリからリクエストするような実装を作れればと思います。