SRE部のm-ishizukaです。今回は夏休みの宿題の成果発表をさせてもらいます。
はじめに
OCSPはよいプロトコルだが、難点が多くGoogleChromeが一時期無効にしていたりしていました。 宇宙の平和1のためにはOCSPを手軽に・高速に・安価に実現する必要がある。
OCSPとは?
- https://ja.wikipedia.org/wiki/Online_Certificate_Status_Protocol
- セキュリティ界隈だとOCSPとOSCPがあり、非常に紛らわしい。
- TLS証明書の失効情報を小さい転送量でやりとりするプロトコル
- 失効情報を一覧にして返すCRLという仕組みもあるが、容量が大きくなりがち
- レスポンス自体が署名されている
- TLS証明書の証明書と考えてもらうとわかりやすいかも
- httpでやりとりしている
ツール
- TypeScript
- HonoがTypeScriptだったため
- Hono
- 勝手の良さげなCloudflare Workersで動かせるフレームワーク
- Cloudflare Workers
- 今回の肝1
- CloudflareにAPIサーバがいっぱい設置してくれる。すごい
- Cloudflare KV
- 今回の肝2
- Cloudflare WorkersからKeyValueがたたける。ありがたい
なお、私はCloudflare Workersも、JavaScript開発も初です。 コードの品質等のツッコミについては何もしませんが勘弁してください。
OCSPの中身
実際にRFCを見る方が正解だが、今回はopensslでパースされたデータを見てみましょう。 リクエストの中身は、後ほど出てくるスクリプトで確認はできます。 io.cyberdefense.jpのサーバ証明書のOCSPレコードを、以下のように取得・パースしたものです。
上から4つは、OCSPを確認したい証明書から、計算ができたり、決め打ちで良い値になっています。
- Hash Algorithm: sha1は必須、sha1以外なら、応答しない・sha1で応答するのどちらだもいいらしい。
- Issuer Name Hash: IssueしたCA証明書のsubject情報をhashに通したもの
- Issuer Key Hash: IssueしたCA証明書の公開鍵情報
- Serial NUmber: Issueされた証明書のシリアルナンバー
さて、問題はopenssl3.0からデフォルトになったNonce(適当な乱数)です。 replay応答対策に存在する拡張なのですが、正直非常に扱いづらいですね。 これも無視しても良いのですが、後のために一応ここで記載しました。
OCSP Request Data:
Version: 1 (0x0)
Requestor List:
Certificate ID:
Hash Algorithm: sha1
Issuer Name Hash: EF900170DD588F2A651E21511FDCD0BB6F512BAB
Issuer Key Hash: 5AF3ED2BFC36C23779B95230EA546FCF55CB2EAC
Serial Number: 03FEEA80BC81A98F3CC43AE6D7F12FF55E2D
Request Extensions:
OCSP Nonce:
04107DD15DD3ABBCEAC9D29AE3DEFD62AF31
OCSPリクエストデータ
OCSPリクエストの中身のデータを、ASN1でバイナリ化されたものをやりとりします。 ASN1でバイナリ化されたものを名義上、OCSPリクエストデータと記述します。 なので、一つ前に記載したOCSPの中身のテキストは、OCSPリクエストデータを人間が読みやすいように記述したものとなります。
OCSPリクエストデータを投げる方法はRFCではHTTPのGETメソッドとPOSTメソッドが定義されています。 base64で通信量が増えてしまうGETよりPOSTのほうが使われるのかもしれません。#多分
- GET: URL Path にbase64したOCSPリクエストデータを入れて投げる
- POST: bodyにOCSPリクエストデータを入れて投げる
OCSPの問題
- staticコンテンツ配信サーバとは相性が悪い
- OCSPリクエストデータをパースして解釈しないといけない
- バイナリデータに拡張仕様があり(無視してもOKだが)拡張の有無でバイナリデータが異なる
- 例: nonceを入れる・証明書アルゴリズム指定など
- キャッシュさせてもあまり効果が少ない(と思われる)
- HTTPヘッダの量と、バイナリ列の量的な意味
- レスポンダに署名をさせようとすると、その署名のための秘密鍵をOCSPサーバに持たせないといけない
- OCSPサーバ側にHSMを触らせるか、OCSPレスポンスデータが大きくなるのを承知で委譲が必要
- OCSPレスポンスのデータサイズが大きくなってしまう(1packetで収まらなくなる)
- OCSPサーバ側にHSMを触らせるか、OCSPレスポンスデータが大きくなるのを承知で委譲が必要
今回の提案手法
- Cloudflare WorkersがOCSPレスポンスを応答
- Workersはバイナリをパースし、中身のシリアルをKVに問い合わせ
- KVの結果をそのまま応答
- KV側への投下はWorkers経由ではなく、別の方法で実現
- 事前に生成したOCSPレスポンスデータを応答するため、Nonceは完全無視
TypeScript内の中身
こちらにデプロイしたコードが。
- データの受け取り:
- GET: URL path クエリのbase64化したOCSPリクエストデータを受け取り、base64 decode
- POST: bodyのOCSPリクエストを受け取る
- ASN1でパース
- 特定の値を取得
- データ処理:
- 自身が管理しているCAの情報と比較
- KeyValueに問い合わせ -> base64デコード -> 応答
提案手法のメリット
- DDoS対策はCloudflareが実施してくれる
- KV/Workersのコストを払うだけで利用可能
- コスト計算が容易
- 事前にKVにOCSPを入れる
- Workersのアクセス数で換算
- HSMを常時稼働させる理由がなくなる(HSMではなく、権限委譲したOCSP CA(ファイルベースの秘密鍵)なら別だが。)
- Let's Encrypt2のやり方に近い。
- 結局こうなりますよねぇ。。。
提案手法のデメリット
- Cloudflareがhttpによるworkersをいつまで応答してくれるか不明
- TLSだと動かなかった。。。
- OCSPがTLSでも動くようになったらいいなぁ。。。けど一般的なグローバルのCAだとただのループになるので無理だろうなぁ。。。とも
- IoTとか組込系でTLSを張るほど処理能力が無い機械が残ればきっとhttpも残ってくれる・・・と信じてます
- KVに入れる方法や、OCSPレスポンスの生成が必要
- CDNを挟むことでHTTPヘッダが大きくなってしまう
叩いてみた
一般のio.cyberdefense.jpの証明書
openssl s_client -connect io.cyberdefense.jp:443 < /dev/null| openssl x509 > server.crt
curl http://e1.i.lencr.org/ | openssl x509 -inform der > intermediate.crt
openssl ocsp -no_nonce -issuer intermediate.crt -cert server.crt -text -url http://e1.o.lencr.org -respout ocsp.resp -reqout ocsp.req
提案手法で問い合わせをした場合
openssl ocsp -no_nonce -issuer intermediate.crt -cert server.crt -text -url http://ocsp.mishi-worker.workers.dev
openssl ocsp -issuer intermediate.crt -cert server.crt -text -url http://ocsp.mishi-worker.workers.dev
curl -H "Content-Type: application/ocsp-request" http://ocsp.mishi-worker.workers.dev/$(cat ocsp.req | basenc --base64 | sed -e 's/\//%2F/g;s/=/%3D/g;s/\+/%2B/g'| tr -d "\n")
※ただし、io.cyberdefense.jpの証明書のシリアルが変わった・OCSPレコードの期限が過ぎたら、動かなくなります。 期限切れなら、期限切れというエラーだけですみますが。
OCSP まとめ
- CDNとの相性がよろしく無い
- クラウドネイティブに作らないとこの先生きのこれない3
- Let's Encrptの設計を見るとヨロシ
- レスポンス生成と、レスポンダの分離
- Server証明書はOCSP stapling有効にしましょ
まとめ
- Cloudflare WorkersとKVにより超格安なOCSPレスポンダの作成
- 現状はまだsha1のみでparse error含めて一切の考慮はしていないが、openssl 3.0のデフォルトオプションでは動くことを確認
- やる気がでたらちゃんとリリースします
おまけ
- 掛かったコスト:
- ドメイン取得代で4$/Year (.win ドメインすごい安い。600円ぐらい)
- 実はworkers.devを使えばドメイン取得すら不要だったと分かってさらに驚愕でした
- ドメイン取得代で4$/Year (.win ドメインすごい安い。600円ぐらい)
- 勉強コスト:
- TypeScriptを初めて触ってみた
- 全体でおおよそ2日ぐらいでデプロイまで至った
以下独り言
1. io.cyberdefense.jpのわけ
www.cyberdefense.jpで利用しているJPRSの証明書のOCSPレスポンダ(DNSレコード的にはセコムらしいですが)が、
- nonce有りの応答をしてくれる
- 応答速度が(少なくとも日本から)高速に応答してくれる
という点で敵わなかったのをここに白状させてもらいます。
2. GET methodでうまく動かない
GET methodでも一応対応できると思われるが、なぜか独自ドメインを捻じ曲げた場合、正常に動作しなかった。 # XXX.XXXX.winを取得し、ocsp.mishi-worker.wokers.devに向くようにしました。 POSTでは動作しましたが、GETではエラーが帰ってきて???となりましたが、とりあえず検証する方法が見当たらなかったのでイイヤ。と諦めました。 error code: 522でした。なんでしょう。私のデプロイ方法が間違っている可能性もあるのでデバッグをしてから結論を出します。