はじめに
背景
以前の「SSHの公開鍵認証における良くある誤解の話」に引き続き、SSHにまつわる誤解に関する話です。
公開鍵暗号とハイブリッド暗号のありふれた説明を聞くと、多くの人が「SSH? 共通鍵を公開鍵暗号で暗号化して渡すのね、オーケーオーケー」と早合点しがちです。しかし実態は全く異なります。
その点を少し具体的に解説します。
TL;DR;
- SSHもSSL/TLSと類似の、鍵交換・認証(ホスト認証)・暗号化・MACの4要素のハイブリッド暗号
- 2種類の公開鍵暗号、鍵交換(鍵交換用)、電子署名(ホスト認証用)が連携して使われる
- 「公開鍵で暗号化」は基本使われない。知識として忘れた方がマシ
注意事項
今回はユーザ認証 ( パスワード認証や公開鍵認証など ) の話は対象外です。
また、SSHと言っても既に過去の存在の SSHv1については対象外です。全て SSHv2 の話です。
なお、動作検証は、x86_64 Windows10 上で、クライアントは WSL1/Ubuntu18.04/OpenSSH 7.6p1 で行っています。
ハイブリッド暗号
構成要素
暗号化通信としてポピュラーな技術として SSL/TLS があり、鍵交換・認証・暗号化・MAC(メッセージ認証コード)の4要素のハイブリッドになっています。
※詳細は「SSL/TLSの基本」で解説しています。
SSH も同じようにこの4要素で構成されており、技術的には大分似ています。
- 鍵交換
- 暗号化・MACで使う共通鍵の元になる秘密情報を共有する機能
- DH,ECDHといった鍵交換(公開鍵暗号の1種)を使う
- ホスト認証
- 接続したサーバが意図通りか ( なりすましでないか ) を確認する機能
- RSA署名,ECDSA,EdDSAといった電子署名(公開鍵暗号の1種)を使う
- 「正しい署名を作れるのは秘密鍵の持ち主だけ」という性質を利用し、署名によって本物のサーバであることを証明する
- クライアントがサーバを識別するキーとなるのはホスト公開鍵
- 暗号化
- 通信データを暗号化し盗聴を防ぐ機能
- AES,ChaCha20といった共通鍵暗号を使う
- MAC(メッセージ認証コード)
- 通信データに改ざん検知用の確認データ(メッセージ認証コード)を付与する機能
- 現在は、AEADとして暗号化機能がMACも兼ねる方式が一般的
- AEADとして、AESのGCMモード,ChaCha20-Poly1305等が使われる
SSL/TLSとの対比
以下は、「SSL/TLSの基本」で挙げた図です。SSH についても機能の位置づけは同じとなります。
ただ、SSL/TLSとSSHで大きく異なる点が1つあります。
SSL/TLSについては、サーバが持つ公開鍵を必ず証明書の形で扱い、PKIによって「どのサーバの公開鍵か」を保証しますが、SSHでは一般に裸の公開鍵を直接扱い、「どのサーバがどんな公開鍵(ホスト鍵)を持っているか」を確認する責任はクライアントが負うことになります。
※なおSSHの主要実装であるOpenSSHでは、SSL/TLSで使うX509形式ではありませんが、裸の公開鍵の代わりに証明書を使う運用も可能になっています。
鍵交換とホスト認証の連携
連携する理由
上で挙げた通り、SSHでは鍵交換で「鍵交換」・ホスト認証で「電子署名」2種類の公開鍵暗号を用います。
しかしこの2つはバラバラに機能するわけではなく連携したものとして、同じ処理の中でまとめて使われます。
その理由もSSL/TLSと同様です。
- 鍵交換からの視点
- 鍵交換に関わるデータに署名をつけることによって保証を与え、第三者によるすりかえを防ぐ
- ホスト認証からの視点
- 署名の対象となるデータに鍵交換のデータを絡めることで、署名の流用を防ぐ
※署名データだけなら第三者も取得可能だが、鍵交換を成立させるデータが揃わないので役に立たない
- 署名の対象となるデータに鍵交換のデータを絡めることで、署名の流用を防ぐ
このように、両機能がお互いの弱点を補う形になっているのです。
処理のイメージ
では、鍵交換・ホスト認証がどのように実施されるのか。イメージとしては次の図のようになり、接続開始時にほぼ真っ先に実施され、その後暗号化通信が開始されます。
2種類の公開鍵暗号に絡んで、鍵交換用の秘密鍵・公開鍵の鍵ペアが2組 ( 図中「交C」「交S」で表記 )、ホスト認証用の秘密鍵・公開鍵の鍵ペア ( ホスト鍵、図中「ホスト」で表記 ) が用いられます。
※サーバは予めホスト鍵を用意しておきます
※2023/4/16 図中の「ホスト鍵確認」の位置が「署名検証」と逆に間違えていたため、図を差し替えました
この図にあるように、細々したやりとりを除けば、公開鍵の送付(クライアント→サーバ)と、返送(サーバ→クライアント)の一往復だけで処理が完了します。
処理のポイントをかいつまんで挙げると次のようになります。
- 鍵交換
- お互いが自分の秘密鍵と相手の公開鍵を組み合わせることで、同一の秘密情報を共有します
※参考:「2つの公開鍵暗号」の「鍵交換」の章 - 共有した秘密情報は、暗号化/MAC用の共通鍵などを作るのに利用されます
- 公開鍵がネットワーク上を流れますが、第三者が盗聴しても同じ秘密情報を作成することができません
- お互いが自分の秘密鍵と相手の公開鍵を組み合わせることで、同一の秘密情報を共有します
- 署名対象
- 鍵交換用のデータも絡めて、両者で同一のデータを作り、署名対象とします
- サーバではホスト秘密鍵による署名の生成、クライアントではホスト公開鍵による署名の検証に使用します
- 署名検証
- 検証が成功すれば、サーバが対応するホスト秘密鍵を持っている証明になります
- また、署名対象には鍵交換で作成した秘密情報も絡むため、同一の秘密情報を共有できたことも同時に分かります
- ホスト鍵確認
- サーバのホスト公開鍵が意図通りか、クライアント側で管理している known_hostsファイル等で確認を行います
- 情報未登録であれば、ここで登録確認が行われるのが一般的です
※正しいホスト鍵かどうかの確認は別途行う必要があります
実際の動作例
上で説明した処理内容ですが、実際にSSHを使っていても表から見えるものではないため、それだけで実感することは難しいと思います。
「公開鍵で共通鍵を暗号化して送る」というデマが後を絶たないのも、実際に見て確認し辛いからという面もあるでしょう。
そこで、WireSharkで通信がどう見えるか、実際にキャプチャしたものを載せます。
ssh -T git@github.com
というコマンドで、githubに初回接続し、Are you sure you want to continue connecting (yes/no)?
というプロンプトが出るまでの部分になります。
- 公開鍵送付(クライアント→サーバ)
画像にある e というパラメータで鍵交換用の公開鍵を送付しているのが分かります
なお、DH と書いてありますが、今回の通信ではECDHの一種Curve25519が使われています
- 公開鍵返送(サーバ→クライアント)
画像にある f というパラメータが鍵交換用の公開鍵、それに加えてホスト公開鍵、署名の3点が揃って返送されているのが分かります
コマンドに対して、冗長化オプション-v
を3重につけたデバッグログでは次のように対応します。
$ ssh -vvvT git@github.com
(略)
debug1: Connecting to github.com [52.69.186.44] port 22.
(略)
debug3: send packet: type 20 ← 事前の情報共有(KEXINIT)
debug1: SSH2_MSG_KEXINIT sent
debug3: receive packet: type 20
debug1: SSH2_MSG_KEXINIT received
(略)
debug1: kex: algorithm: curve25519-sha256 ← 鍵交換アルゴリズム(ECDHの一種)
debug1: kex: host key algorithm: rsa-sha2-512 ← ホスト鍵の署名アルゴリズム
debug1: kex: server->client cipher: chacha20-poly1305@openssh.com MAC: <implicit> compression: none
debug1: kex: client->server cipher: chacha20-poly1305@openssh.com MAC: <implicit> compression: none
debug3: send packet: type 30 ← 公開鍵送付(KEX_ECDH_INIT)
debug1: expecting SSH2_MSG_KEX_ECDH_REPLY
debug3: receive packet: type 31 ← 公開鍵返送(KEX_ECDH_REPLY)
debug1: Server host key: ssh-rsa SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8
debug3: hostkeys_foreach: reading file "/home/angel/.ssh/known_hosts" ← known_hostsの確認
debug3: hostkeys_foreach: reading file "/home/angel/.ssh/known_hosts"
The authenticity of host 'github.com (52.69.186.44)' can't be established.
RSA key fingerprint is SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8.
Are you sure you want to continue connecting (yes/no)? ← ホスト鍵未登録のためユーザに確認
最初に情報を交換して、鍵交換にCurve25519、ホスト認証にRSA署名、暗号化/MACにChacha20-Poly1305を使うことを同意したのち、公開鍵送付(メッセージタイプ30番)、公開鍵返送(メッセージタイプ31番)を行い、known_hostsでのホスト鍵確認を行っていることが分かります。
流石に秘密情報がどうなっているか、そういった点は表に出ませんが、通信内容からでもウラを取ることはある程度可能かと思います。
余談
公開鍵暗号の説明のマズさ
一般に公開鍵暗号の説明がある場合、「共通鍵暗号とは~、それに対して公開鍵暗号は~」という説明が必ずと言って良いほどあります。
しかし、この記事で見た通り、そんな説明を覚えてもSSHの理解には全く役に立ってません。
つまるところ、一般に見る「公開鍵で暗号化して秘密鍵で復号します」という説明は、公開鍵暗号の基礎ではないと言えます。
もっと基礎的な部分としては、「2つの公開鍵暗号」の「概要」で挙げたような特徴の方が適切と言えるでしょう。
マトモな理解を目指すのであれば、一旦「公開鍵で暗号化して~」という説明は、むしろ忘れてしまった方が良いと思います。
おわりに
公開鍵認証と違い、ホスト認証・鍵交換は若干図にし辛いのでためらっていたのですが、折角なので作ってみました。たった一往復のやりとりで処理を完結する様は、結構芸術的で個人的には好きです。
この記事や関連する記事等々で誤解が解消されていけばなあ、と思っています。