Chienomi

SSHの公開鍵認証に関する誤謬と実際

技術::security

SSHの公開鍵認証について、「常識」となっている説明がある。

「サーバーが公開鍵によって暗号化したデータを送り、クライアントはこれを復号化して復号化した結果を送信する」 というものだ。

ものすごく当たり前のようにそう言われているし、ウェブでも本でもその説明ばかりであり、私も雑誌でそのように読んだのでそう信じていた。

が、これは事実ではない。

よく考えてみるとおかしな点がいくつかある。 例えば、サーバーがいきなり公開鍵での暗号化を行うというのはちょっとおかしい。サーバーは複数の公開鍵を候補としているわけで、片っ端から暗号化して送りつけるか、もしくは事前にクライアントから使うべき公開鍵を指定されていなければならない。しかし、この説明ではこの手順がない。 さらに、RSAは確かに署名も暗号化も可能なアルゴリズムだが、DSA, ECDSA, EdDSAはいずれも「署名アルゴリズム」であり、暗号化自体ができない。

私はお仕事でSSHについての授業のための教材を書いていたとき、裏取りとしてRFCを読んでいておかしいことに気づいた。 ほとんどの人は何かのきっかけで「おかしい」と気づかなければずっと気づかないままになるようなものだろう。

実際

プロトコル

SSHのプロトコルについては基本的に理解しているという前提で進めるが、一応説明しておこう。

SSHはバイナリプロトコルであり、バイナリプロトコルということは「長さ」を特定する方法が必要になる。

SSHパケットは先頭4バイトがパケット長で、次の1バイトがパディング長、それ以降がペイロードになる。 パケット全体の長さは8の倍数バイトになっている。 パディングはランダムな値で、結果8の倍数バイトになるように長さが設定される。

ペイロードの先頭は1バイトのメッセージタイプ(整数)になっている。 それぞれのメッセージタイプの値に対して名前がついている。

SSHサーバーは動的にクライアントに問いかけることはなく、接続を確率するまでは常にクライアントからの問いかけに応答する形である。

認証

クライアントはSSH2_MSG_SERVICE_REQUEST (Type 5)を送信し、サーバーが認証を受け付ける場合、SSH2_MSG_USERAUYTH_ACCEPT (Type 6)を返す。

認証はクライアントがSSH2_MSG_USERAUTH_REQUEST (Type 50)によって認証を行い、それを受け取ったサーバーはSSH2_MSG_USERAUTH_FAILURE (Type 51)またはSSH2_MSG_USERAUTH_SUCCESS (Type 52)を返す。

失敗した場合に失敗した理由は回答されないが、有効な認証メソッドの一覧が返る。 これを先に得ようとする場合、クライアントは認証メソッドがnoneであるSSH_MSG_USERAUTH_REQUESTを送り、SSH2_MSG_USERAUTH_FAILUREを受け取って認証を選択する。

認証を複数突破する必要がある場合、途中の認証に成功した場合もSSH2_MSG_USERAUTH_FAILUREが返る。

公開鍵認証

認証メソッド publickey を使用する場合、クライアントは認証メソッドpublickeyで、認証メソッド名の直後に真偽値があり、この真偽値が偽であるSSH2_MSG_USERAUTH_REQUESTを送信する。 サーバーはこれに対して、その鍵によるpublickey認証ができないことを示すSSH2_MSG_USERAUTH_FAILUREか、もしくはpublickey認証が可能であることを示すSSH2_MSG_USERAUTH_PK_OK (Type 60)を返す。 このやりとりは省略できる

このやりとりはpublic key blobによってクライアントは認証に使用する公開鍵そのものを送ることができる。 この場合、サーバーはその鍵が認証に使えるか否か(その鍵で認証を行うと正しい署名であればアクセス権限を得られるか)を回答する。これがSSH2_MSG_USERAUTH_FAILUREまたはSSH2_MSG_USERAUTH_PK_OKになる。このSSH2_MSG_USERAUTH_REQUESTにuser nameも含んでおり、サーバーはAuthorizedKeysFileを参照してその鍵が認証可能かどうかを確認する。

このやりとりの意味は複数あるが、特に大きな点としては認証回数にあるだろう。SSHでは認証を試みることができる回数に制限がかけられていることもあるが、手持ちの鍵を片っ端から試したらそれにひっかかる可能性は高い。対して、このやりとりは認証そのものは行っていないから、それに抵触しない。 また、秘密鍵が暗号化されている場合に、まず署名するとなると片っ端から鍵の暗号化を解除していかなければならなくなる。それに、秘密鍵による署名という重めの処理を無駄に繰り返すのは効率的とは言えない。

次に真偽値が真であるSSH2_MSG_USERAUTH_REQUESTを送信する。 これに続き、

  • 公開鍵暗号アルゴリズム (String)
  • 公開鍵 (String)
  • 署名された値 (String)

が後続する。アルゴリズムについてはDHにおいて交換されているが、これに従う必要はない。

署名された値は、セッションIDの後ろにこのパケットの署名よりも前の部分を結合したもの全体に対する署名である。 DHで交換された経路暗号を担う共通の秘密1であるセッションIDを使用していることにより、その値そのものが他者には知りえない共通の秘密になっている。 忘れてはいけない。 SSHの経路暗号は共通鍵暗号である

パケットには公開鍵そのものを含み、署名の検証は付属された公開鍵によって行われる。 それが成功した場合に、その認証に成功した公開鍵が認証済みであるかどうか(authorized_keysに存在するか)を照合するのである。 鍵オプションの適用もこれを経て行われる。

公開鍵認証についての解説

噛み砕くと、この認証は次のように機能する:

  1. このセッションを確立した者でない不正な第三者が割り込んで認証しようとした場合、署名された値が正しくないのでFAILUREとなる
  2. 自身が秘密鍵を持たない公開鍵によって認証を行おうとした場合、署名が検証できないのでFAILUREとなる
  3. 公開鍵によって値が正しく検証され、かつ値が正しいとき、AuthorizedKeysFileによってアクセス権限を与えられる。AuthorizedKeysFileに当該公開鍵がない場合、アクセス権限がないためFAILUREとなる

勘違いしてはならないのは、DH鍵交換は認証ではなく、経路暗号において確認されるのはサーバーホストがクライアントから見て意図するものであるかどうかということだけであり(現実にはそれもほとんど確認されていない2が)、サーバーに対するアクセス権の有無に関わらず経路暗号は確立され、正当な共通の秘密が発生するということだ。 だから、正しい経路で、正しい手順で公開鍵認証を行うことは、サーバーへのアクセス権がなくてもできる。しかし、それにおいて公開鍵の正当性が検証されたところで、次のステップであるその公開鍵に対するアクセス権の認証が通らない、ということだ。

もし、サーバーが(クライアントから送られた公開鍵でなく)AuthorizedKeysFileにある公開鍵で検証するならば、検証しなければならない鍵の数を増やすことになる(登録された全ての鍵で検証しなければならなくなる)。そもそも合理的な動作とは言えないだろう。 与えられた公開鍵による署名の検証を行う前にその公開鍵が権限を持つものかどうか確認しないのか、という点については、「実装次第」である。そのように振る舞うほうが処理は軽いかもしれない。

OpenSSHの場合は接続に指定を必要とするのは秘密鍵だけであるが、OpenSSHの場合、秘密鍵と同名で.pubをつけたファイルが存在するならば、それを公開鍵として使う。なければ、OpenSSHの秘密鍵は公開鍵を含んでいるので、ここから公開鍵を取り出して使用する。もしも.pubをつけたファイルが秘密鍵に対応していないものであるならば、当然ながら秘密鍵で行った署名が同梱した公開鍵で検証できないため認証に失敗する(現行のOpenSSHでは、これは警告される)。

DH鍵共有、あるいはECDH鍵共有においても共通の秘密を共有するために非対称暗号が用いられる。 だが、このために使用される鍵は一時的なもの(DHE/ECDHE)で、認証に使う鍵ではない。

なぜこうなった

おそらく、SSH1時代のRSAAuthenticationというのと混同しているのだうろ。

RSAAuthenticationは、チャレンジレスポンス方式で、よく言われる方式と似ている。 そして、SSH1時代の解説を誰も検証することなくコピペで広め続けたために、それが適合しなくなった現代においても流布されている、ということではないか。

RSAAuthenticationはPublickey Authenticationに置き換わる形でSSH2からは消えている。 今やOpenSSHにおいてはSSH1はサポートされなくなっているので、あまり意味のない知識だ。


  1. 「共通の秘密」が何を意味するかが分からない人は、「対称暗号における共通鍵」という認識で良い。それでも分からない人は、Wikipediaで「デッフィー・ヘルマン鍵共有」を読むと良いだろう。↩︎

  2. DH鍵交換は中間者攻撃に無力である。SSHではこれに対応するためfingerprintとknownhostというものを使っている。これが異なっていれば「信用したホストとは異なるホストと鍵交換をしようとしている」という判定をするわけだが、そもそも初回接続時に関してはそのfingerprintが信用すべきホストのものなのかがわからない。そのため、ユーザーに判断を求めるのだが、初回接続時にfingerprintの検証をしている人はほとんどいないだろう。↩︎