Chienomi

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

技術::security

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

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

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

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

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

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

なお、一部の内容がOpenSSHに基づいた内容となっていることもご了承いただきたい。

実際

プロトコル

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

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

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

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

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

鍵交換

鍵交換(Key Exchange=KEX)は暗号経路を確立するためのものである。

一般的にこの処理をSSHの解説において「DH」や「DH鍵交換」といった表現が使われることもある。 DH(Diffie–Hellman key exchange)はこの鍵交換アルゴリズムのひとつであり、SSHでは一般的なものである。

ただし、SSHにおいて鍵交換=DH鍵交換を意味するわけではない。 OpenSSHの場合、KexAlgorithemsによって設定可能であり、デフォルトは

curve25519-sha256,curve25519-sha256@libssh.org,
ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,
diffie-hellman-group-exchange-sha256,
diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,
diffie-hellman-group14-sha256

である。 diffie-hellmanとあるものがDHを用いるもので、curve25519やECDHと書かれているものはECDH(楕円ディフィー・ヘルマン鍵交換)を用いるものである。 このことからも分かるように、「SSHの鍵交換=DH鍵交換ではない」どころか、むしろ「今のSSHの鍵交換はECDHを使うのが普通、DHはあまり使わない」とすら言える(もちろん、ECDHはDHの一種、と主張することはできる)。

OpenSH_8.4p1/OpenSSL 1.1.1においてはさらに

% ssh -Q KexAlgorithms 
diffie-hellman-group1-sha1
diffie-hellman-group14-sha1
diffie-hellman-group14-sha256
diffie-hellman-group16-sha512
diffie-hellman-group18-sha512
diffie-hellman-group-exchange-sha1
diffie-hellman-group-exchange-sha256
ecdh-sha2-nistp256
ecdh-sha2-nistp384
ecdh-sha2-nistp521
curve25519-sha256
curve25519-sha256@libssh.org
sntrup4591761x25519-sha512@tinyssh.org

というアルゴリズムがサポートされている。

鍵交換の主目的は、経路暗号化に使う共通鍵を生成し、共有することである。 DHやECDHでは、最初の段階で非対称暗号を用いて経路を暗号化し、この状態で鍵交換を行う。

だが、この鍵は認証で使用する(つまり、OpenSSHにおいて ssh -i などと指定する)鍵ではない。あくまで一時的な経路暗号化のための鍵である。 さらに、KEXを終えたときには、共通鍵暗号を用いて経路は暗号化される。

認証

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

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

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

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

公開鍵認証

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

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

このやりとりの意味は複数あるが、例えば秘密鍵が暗号化されている場合に、まず署名するとなると片っ端から鍵の暗号化を解除していかなければならなくなる。それに、秘密鍵による署名という重めの処理を無駄に繰り返すのは効率的とは言えない。

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

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

が後続する。KEXの冒頭であるSSH_MSG_KEXINITでは公開鍵アルゴリズムを交換している。 これはこの公開鍵認証同様の署名アルゴリズムであるが、あくまでもサーバー側のホスト鍵の確認に用いるものであり、 この公開鍵認証ではこれに制約されない。 また、拡張ネゴシエーションを用いて SSH_MSG_EXT_INFOserver-sig-algsによって利用可能な署名アルゴリズムを事前に知ることもできる。 ただし、これはサーバーによって利用可能なアルゴリズムが列挙されるというものであり、また全てのサーバーがアルゴリズムの列挙を行うわけではない。 サーバーがそのようにできる、というだけのことだ。クライアントは任意のアルゴリズムによって署名を行って送ることができる。

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

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

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

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

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

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

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

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

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

なぜこうなった

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

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

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

参考文献

RFC

  • S. Lehtinen and C. Lonvick, The Secure Shell (SSH) Protocol Assigned Numbers, RFC 4250, January 2006.
  • T. Ylonen and C. Lonvick, The Secure Shell (SSH) Protocol Architecture, RFC 4251, January 2006.
  • T. Ylonen and C. Lonvick, The Secure Shell (SSH) Authentication Protocol, RFC 4252, January 2006.
  • T. Ylonen and C. Lonvick, The Secure Shell (SSH) Transport Layer Protocol, RFC 4253, January 2006.
  • T. Ylonen and C. Lonvick, The Secure Shell (SSH) Connection Protocol, RFC 4254, January 2006.
  • J. Schlyter and W. Griffin, Using DNS to Securely Publish Secure Shell (SSH) Key Fingerprints, RFC 4255, January 2006.
  • F. Cusack and M. Forssen, Generic Message Exchange Authentication for the Secure Shell Protocol (SSH), RFC 4256, January 2006.
  • J. Galbraith and P. Remaker, The Secure Shell (SSH) Session Channel Break Extension, RFC 4335, January 2006.
  • M. Bellare, T. Kohno, and C. Namprempre, The Secure Shell (SSH) Transport Layer Encryption Modes, RFC 4344, January 2006.
  • B. Harris, Improved Arcfour Modes for the Secure Shell (SSH) Transport Layer Protocol, RFC 4345, January 2006.
  • M. Friedl, N. Provos, and W. Simpson, Diffie-Hellman Group Exchange for the Secure Shell (SSH) Transport Layer Protocol, RFC 4419, March 2006.
  • J. Galbraith and R. Thayer, The Secure Shell (SSH) Public Key File Format, RFC 4716, November 2006.
  • D. Stebila and J. Green, Elliptic Curve Algorithm Integration in the Secure Shell Transport Layer, RFC 5656, December 2009.
  • J. Postel, J. Reynolds and ISI, FILE TRANSFER PROTOCOL (FTP), RFC 959, October 1985.
  • M. Horowitz, Cygnus Solutions, S. Lunt and Bellcore, FTP Security Extensions, RFC 2228, October 1997.
  • P. Ford-Hutchinson and IBM UK Ltd, Securing FTP with TLS, RFC 4217, October 2005.
  • D. Bider, Bitvise Limited, Extension Negotiation in the Secure Shell (SSH) Protocol, RFC 8308, March 2018

manpages

  • ssh(1)
  • sftp(1)
  • sshd_config(5)
  • ssh_config(5)
  • ssh-keysign(8)

その他

指摘

直接、おかしな記述や誤りについてご指摘いただいた。 この場を借りて改めて御礼申し上げたい。

本当にありがとうございます。