SSL/TLS そしてピンニング
きっかけ
副業で Web3 の wallet server と client の通信の暗号化の話になった。その上で、どれほど堅牢にすれば秘匿性の高いデータを送受信するに足るのかが気になったので、まずは理解があやふやなこの辺りの基礎を理解しようと考えた。
この記事を元に要点をまとめる。もしかしたらほとんど模写になるかもしれんが、それはそれでいい。
SSL/TLS
3 つの役割がある。
盗聴を防止する
前提として SSL/TLS で通信をすることで通信内容は暗号化されるため、途中で見られても中身は分からないし、第三者は意味のあるデータを取得することはできない。
改竄を防止する
SSL/TLS では受け取ったデータが、送信時と同じ内容かどうかを確認することができる。
接続先のサーバーが本物かを確認する
サーバー証明書を利用して、実際に接続するサーバーが本物かどうかを確かめる。
サーバー証明書
そのドメインの持ち主がそのサーバーであることを保証するもの。サーバー証明書には主に下記が書かれている。
- ドメイン
- 有効期限
- 発行主 (認証局/CA)
- サーバー公開鍵
証明書は公開情報であり、ブラウザのデベロッパーツールから閲覧できる。また、詳細タブを押すと色々と細かく何が含まれているかが見れる。

サーバー証明書は以下を満たす必要がある。
- 信頼された第三者が発行していること
- 第三者を端末が信頼していること
前者の、第三者とは認証局 (Certificate Authority) のこと。事前にそのサーバーがドメインの正当な持ち主であるということを伝えることで、証明書に電子署名をしたサーバー証明書を発行してくれる。
そして、Web ブラウザや OS にはあらかじめ信頼して良い認証局の証明書が登録してあります。そのため、通信を開始する際にサーバー証明書を受け取ると
- 発行主 (CA)
- 発行主は信頼済みか
- 証明書の内容に問題がないか
の確認がなされます。
認証局の署名の仕組み
これから自分が発行する証明書の内容 (ドメイン名や有効期限) を元にハッシュ値を計算する。また、計算したハッシュ値を CA 自身の秘密鍵を利用して署名します。この秘密鍵で署名 (暗号化) されたハッシュ値が、証明書の中に書かれている電子署名のこと。
サーバーが本物かをどう確認するか
結論、2 通りの方法でハッシュ値を導き出して、それぞれの値が一致していることを確認することで本物とみなす。
1 つ目。受け取ったサーバー証明書を元に、ローカルでもハッシュ値を計算する。アルゴリズムが証明書に書いてあるので、証明書をそのアルゴリズムで暗号化してハッシュ値を導き比較する。ということみたい。
また先ほど
また、計算したハッシュ値を CA 自身の秘密鍵を利用して署名します。
と書いたが、Web ブラウザや OS に元々登録してある信頼済み認証局の証明書から公開鍵を取り出すことができます。つまり、その公開鍵を利用することで、認証局の秘密鍵によって電子署名 (= 暗号化されたハッシュ値) を復号することができる。
TLS ハンドシェイクの流れ (TLS1.2 RSA 鍵交換)
クライアントが接続を開始する
SSL3.0, TLS1.1, TLS1.2... みたいに、自分が利用できる暗号方式のリストを送る。
サーバーが証明書を送る
証明書と、使用する暗号方式の方法をクライアントに返す。
サーバー証明書の検証
さっきの話。2 通りの方法でハッシュ値を導き出して比較する。
通信に利用する共通鍵の元となる値を作る
クライアントによってプリマスタシークレットというランダムな値を払い出す。
プリマスタシークレットをサーバーの公開鍵で暗号化して送る
サーバー証明書にはサーバーの公開鍵が含まれているので、その鍵でプリマスタシークレットを暗号化してサーバーに送る。
サーバーが秘密鍵で復号する
はい、復号します。そして、プリマスタシークレットを利用して共通鍵を作成する。以降、この共通鍵によって通信を暗号化/復号する。共通鍵を通信で送受信しなくても良いというのがミソ。
で、私は別に共通鍵はまんまプリマスタシークレットを使えばいいじゃないと思ったのだけれど、それでも暗号が破られるということではないみたい。ただ単に実用上の理由で共通鍵を作っているということらしい。細かくは調べていないけれど、共通鍵は複数作り場所によって利用する共通鍵が異なるみたい。
暗号化された通信が開始される
以降、共通鍵で暗号化されたデータが飛ぶようになる。
HTTPS の改竄検知
通信で送信するデータ + 共通鍵 を利用して、MAC (Message Authentication Code) というハッシュ値を計算する。そして、データと一緒にその MAC もおくる。
受信側は受け取ったデータと手元の共通鍵を利用して MAC を計算する。そして受け取った MAC と一致するかを見るということ。
FAQ (RSA 鍵交換の前提)
本物のサーバー証明書を偽サーバーに設置すれば偽装できるか
できない。偽サーバーは本物サーバーの秘密鍵を知らないので、復号してプリマスタシークレットを得ることができない。つまり共通鍵を手元で作れないので、
サーバーが秘密鍵で復号する
の箇所でハンドシェイクに失敗する。
本物サーバー証明書のドメインを書き換えて偽サーバーに設置すれば偽装できるか
証明書のドメインは example.com ではなく attacker.com にする。この場合、
自分が発行する証明書の内容 (ドメイン名や有効期限) を元にハッシュ値を計算する。また、計算したハッシュ値を CA 自身の秘密鍵を利用して署名します。
の通り、証明書を書き換えると偽サーバー証明書が持つハッシュ値と記載内容が一致しなくなるため、クライアント側でのハッシュ値比較フェーズでハンドシェイクに失敗する。
偽サーバーの正規証明書を偽サーバーに設置しそれを返却する場合偽装できるか
証明書のドメインは attacker.com であり、example.com にリクエストを飛ばしたはずなのに返却された証明書のドメインが attacker.com の場合、TLS コンテキストに保存されていた example.com と attacker.com が比較されて当然違うのでハンドシェイクには失敗する。
認証局を OS に信頼済みとして登録させた状態で偽サーバーで正規サーバーの証明書を作り換えた場合
ピンニングしていない場合終わりです。
まず、この VPN (実体は VPN の顔をした MITM Proxy) を使うならこの CA を信頼してね。と言って OS に信頼済みとして認識させる。クライアント - VPN - exaple.com という経路上で、まずクライアントからのリクエストを VPN が受け取って、そのまま example.com へ飛ばす (この際はまだ中身は見れない)。そして、example.com は VPN にサーバー証明書を返却する。VPN は偽証明書を生成する。
- ドメイン
- 有効期限
- 発行主 (認証局/CA)
- サーバー公開鍵
サーバー証明書の中身のうち、上記の発行主、サーバー公開鍵を VPN のものにぬるっとすり替える。また、当然電子署名も VPN のものでし直す。そしてこの証明書をクライアントに返す。クライアントは VPN の公開鍵を使ってプリマスタシークレットを暗号化して送ってくるので、VPN の秘密鍵で復号して共通鍵作ってゲームオーバーです。あとは全部覗き見し放題。
ピンニングしていれば、ユーザーが勝手に VPN 入れて信頼させたとしてもアプリケーション側でその CA を許さないという前提になっているので問題ない。
ボディやヘッダーの暗号化は必要なのか
例えば HTTPS の終端が LB の場合、LB -> application server の経路は平文で通信が行われる。その場合は意図せぬタイミングでログに秘匿データが漏れてしまったり、クラウドのマルウェア感染の影響を受けたり、クラウドベンダー自体が悪意がある場合に問題がある。
LB -> application server を HTTPS でやり取りさせる場合。こうしてしまえば、あとは LB で復号化されてから再度暗号化されるまでの間にログに出ちゃったりする可能性があるくらいになる。
ボディ / ヘッダーを暗号化してしまえば、復号するタイミングは application server に限定できるので、これさえも防げる。あとは application server で復号したあとはすぐに GC に回収されるようグローバル変数で持ったりするようなことはしないよう注意したり、ログに出さないようにしたりすれば良さそう。
ただヘッダーまで暗号化してしまうと LB がルーティングに利用する情報まで隠れてしまうので、ボディのみという割り切りになることも多いみたい。