🔐

SSHの公開鍵認証とは何をやっているのか?

2023/01/15に公開
12

はじめに

この記事は、SSHでのログインに公開鍵認証を使っているけど仕組みは知らないんだよな、という方に向けて書いたものです。

少し長いですが半分くらいは補足です。必要な部分だけ読んでいただければと思います。

公開鍵認証によるログイン手順

まずは公開鍵認証によるログイン手順を振り返っておきます。

  1. クライアントで秘密鍵と公開鍵のペアを生成
  2. 公開鍵をサーバーに登録
  3. SSH接続を試みる
  4. 初回であれば何か聞かれてよく分からないままyesを選択
  5. ログイン成功

だいたいこんな感じではないでしょうか。なお、4はパスワード認証の場合でも聞かれますね。

公開鍵認証の仕組み

公開鍵認証には署名(デジタル署名)という技術が使われています。

まずは署名について説明し、次に署名を使ったユーザー認証について説明します。

署名

署名という技術には二種類の登場人物が存在します。署名者と検証者です。

署名者は、データに対して署名という処理を施します。すると署名という値が出力されます。署名というのは技術の名前であり、処理の名前でもあり、その結果として出力される値の名前でもあるのです。検証者は署名を検証することで、データが改竄されていないこと、署名者が誰なのかを確認できます。

署名を施すには署名鍵というものが必要です。一方で署名を検証するには検証鍵というものが必要です。

それでは署名の流れを見ていきましょう。

  1. (署名者)署名鍵と検証鍵のペアを生成
  2. (署名者)検証鍵を検証者に渡す
    • 署名鍵は署名者一人だけの秘密にする
    • 検証者は検証鍵の元々の持ち主(=検証鍵とペアになる署名鍵の持ち主)を覚えておく
  3. (署名者)署名鍵を使い、署名対象のデータに署名
    • 署名という値を生成
    • 署名対象のデータを上書きしているわけではない
  4. (署名者)署名対象のデータと署名を検証者に送信
  5. (検証者)検証鍵を使い、署名を検証

署名の検証に成功すると、検証者は以下のことが言えます。

  • 署名対象のデータが改竄されていない
  • 署名は、自身が持つ検証鍵とペアになる署名鍵で生成された

検証者は検証鍵の元々の持ち主(=検証鍵とペアになる署名鍵の持ち主)を覚えているのでしたね。しかも署名鍵は署名者一人だけの秘密です。したがって検証者は署名したのが誰なのか特定できるわけです。

署名を使ったユーザー認証

察しの良い方はお気付きかもしれませんが、公開鍵認証における秘密鍵とは署名鍵、公開鍵とは検証鍵のことです。つまりクライアントが署名を生成し、サーバーがそれを検証することで本人確認しているのです。

公開鍵認証の流れは以下です。

  1. (クライアント)秘密鍵と公開鍵のペアを生成
  2. (クライアント)公開鍵をサーバーに渡す
  3. クライアントとサーバーが、セッション識別子という予測不能な値を共有
  4. (クライアント)秘密鍵を使い、セッション識別子を含むデータに対する署名を生成
    • 署名対象にセッション識別子を含むことで毎回異なる署名を生成
  5. (クライアント)公開鍵と署名をサーバーに送信
  6. (サーバー)受け取った公開鍵が事前に登録済であることを確認
  7. (サーバー)公開鍵を使い、署名を検証 → 検証成功なら認証

セッション識別子についての補足

セッション識別子はクライアントとサーバーがお互いに交換するデータから生成されます。そのためクライアントにもサーバーにも予測不能です。ただし時系列的にはサーバーが先にセッション識別子を得ます。

セッション識別子を共有するまでの流れは以下です。

e と f の内容については割愛します。

初回接続時の確認メッセージは何?

SSHでの初回接続時には以下のような確認メッセージが表示されます。

The authenticity of host 'hoge (xxx.xxx.xxx.xxx)' can't be established.
ECDSA key fingerprint is ~~~~~.
Are you sure you want to continue connecting (yes/no)?

これを理解するにはホスト認証について知らなくてはなりません。

ホスト認証

ホスト認証とは接続先のサーバーが本物であるかをクライアントが確認することです。意識していない方が多いかもしれませんが、ホスト認証はユーザー認証より前に実施されています。そしてホスト認証の中でも署名が使われています。つまりサーバーが自身の秘密鍵で署名を生成し、クライアントがサーバーの公開鍵で署名を検証しています。

ホスト認証はセッション識別子の共有と同時に実施されます。やや込み入っていますが、流れは以下です。

ユーザー認証では、クライアントの公開鍵を事前にサーバーに共有する必要がありました。一方でホスト認証では、サーバーの公開鍵を初回接続時にクライアントに共有することが可能です。

確認メッセージの内容

初回接続時の確認メッセージはホスト認証の図の8番のタイミングで表示されます。つまりサーバーの公開鍵に関するメッセージです。

では一行ずつ見ていきましょう。

The authenticity of host 'hoge (xxx.xxx.xxx.xxx)' can't be established.

IPアドレスがxxx.xxx.xxx.xxxであるhogeというサーバーをクライアントが信用できなかったことを示しています。初回なので仕方がないですね。

ECDSA key fingerprint is ~~~~~.

ECDSAというのは署名アルゴリズムの名前です。他にはRSAED25519といった名前が表示される可能性があります。これはあくまでホスト認証で使用する署名アルゴリズムです。ユーザー認証で使用する署名アルゴリズムとは異なっていて構いません。

fingerprintはハッシュ値のことです。~~~~~の部分にサーバーの公開鍵のハッシュ値が表示されます。本来はサーバーの管理者にハッシュ値が正しいか確認すべきですが、実際に確認している方は少ないと想像します。なおハッシュ値についての説明は割愛します。

Are you sure you want to continue connecting (yes/no)?

yesを選択するとサーバーに接続できます。それと同時にサーバーを信用したことになり、クライアントにサーバーの情報(名前やIPアドレス)と公開鍵がセットで登録されます。

二回目以降の接続

二回目以降の接続では、サーバーから送信される公開鍵がクライアントに登録済であるかを確認します。もちろんこれだけではホスト認証は完了しません。サーバーの公開鍵で署名を検証することでホスト認証が完了します。

なおサーバーの公開鍵が変わった場合は署名を検証するまでもなくホスト認証に失敗します。サーバーから送信される公開鍵が、クライアントに登録されているものと一致しないからです。その場合は必ずサーバーの管理者に公開鍵が変わったかを確認したうえで、クライアントに登録されているサーバーの公開鍵を削除して新しい公開鍵を受け入れましょう。偽物のサーバーが名前やIPアドレスを詐称している可能性もあるため、何の確認もせずに新しい公開鍵を受け入れてはいけません。

よくある誤解についての補足

ここまでの説明を読んで、以下のように思った方がいるかもしれません。

「署名って、暗号化するやつでしょ」

「セッション識別子って、チャレンジとも呼ぶよね」

これらは不正確です。

まず、署名と暗号化は異なる技術です。いくつかある署名アルゴリズムの中には、暗号化と似たような処理を含むものもありますが、ただそれだけです。少しややこしいのは、署名が「公開鍵暗号」と呼ばれる技術に属する点です。「やっぱり暗号化じゃないか!」と思った方がいるかもしれませんが、そうではありません。これについては別で記事を書いていますので、気になる方は読んでみてください。

https://zenn.dev/tetsu1008/articles/1e3673ca1ece42

上の記事内でも紹介していますが、サイボウズ・ラボの光成さんの記事も参考になります。

https://blog.cybozu.io/entry/2021/12/28/080000

なお公開鍵認証の説明で署名という言葉すら使わずに暗号化とか言っているものは大嘘です。あまりに広まりすぎた大嘘なので本当によく見かけます。

次に、セッション識別子をチャレンジとは呼びません。チャレンジは「認証する側が生成して認証される側に送信する値」を指す場合が多いです。一方でセッション識別子は認証する側にもされる側にも予測できない値であり、どちらか一方が生成して送るというものではありません。

参考

https://www.rfc-editor.org/rfc/rfc4251

https://www.rfc-editor.org/rfc/rfc4252

https://www.rfc-editor.org/rfc/rfc4253

Discussion

angel_p_57angel_p_57

ちゃんと調べて「デジタル署名」として説明している人は希少なので、素晴らしいことだと思います。
一点、

そしてホスト認証の中身は署名を使った公開鍵認証です。

について、それまでの説明で「公開鍵認証でセッション識別子を使う」とあるところ、ホスト認証時点ではセッション識別子はまだないので同じような方法はとれないはずで、説明として言い過ぎとなっています。「公開鍵認証のように署名を使った認証を行う」くらいなら問題ないと思いますが。
RFCをご覧になる方が話が早いとは思いますが、概要は「SSHのハイブリッド暗号に関する良くある誤解の話」という記事にまとめてます。ご参考まで。

てつてつ

ありがとうございます。

RFC 4253の8. Diffie-Hellman Key Exchangeに目を通したのですが、

  1. S generates a random number y (0 < y < q) and computes
    f = g^y mod p. S receives e. It computes K = e^y mod p,
    H = hash(V_C || V_S || I_C || I_S || K_S || e || f || K)
    (these elements are encoded according to their types; see below),
    and signature s on H with its private host key. S sends
    (K_S || f || s) to C. The signing operation may involve a
    second hashing operation.

という部分が、サーバはH( = exchange hash = session identifier)に対して署名しているように読めてしまいます。どのように理解するのが正しいのでしょうか?

サーバーが署名する段階ではすでにクライアントのDH公開鍵はサーバーに渡っており、セッション識別子を生成可能という認識でした。

なお7.2. Output from Key Exchange

The exchange hash H from the first key exchange is
additionally used as the session identifier, which is a unique
identifier for this connection.

から、H = exchange hash = session identifierだと考えました。

angel_p_57angel_p_57

すいません。回答遅くなりました。

サーバはH( = exchange hash = session identifier)に対して署名しているように読めてしまいます。どのように理解するのが正しいのでしょうか?

すいません。その解釈で良いと思います。そこは私の認識が甘い所でした。
※ てっきりハッシュから何かしら導出してセッションIDにしてると思い込んでいました。

そうすると、ホスト認証もユーザ認証としての公開鍵認証も、セッションIDを使ってることには変わりないので、元の記事の内容のままでも別に問題なかったですね。失礼しました。

てつてつ

「公開鍵認証」と言った場合にはユーザー認証のことを指す場合が多そうなので、誤解を招かないように

そしてホスト認証の中でも署名が使われています。つまりサーバーが自身の秘密鍵で署名を生成し、クライアントがサーバーの公開鍵で署名を検証しています。

と変更しました。

HikaeHikae

非常に分かりやすく勉強になりました。

ホスト認証はユーザー認証の前に実施されています。

の部分で疑問があります。
これは、ホスト認証の「10)署名の検証」が成功したあとは(セッション識別子は共有済みのため)ユーザー認証の「4)セッション識別子を含むデータに署名」に移行する、という認識であっていますでしょうか?

てつてつ

コメントありがとうございます。

ホスト認証の「10)署名の検証」が成功したあとは(セッション識別子は共有済みのため)ユーザー認証の「4)セッション識別子を含むデータに署名」に移行する、という認識であっていますでしょうか?

前後関係としてはホスト認証よりもユーザー認証のほうが後です。ただしホスト認証の直後にユーザー認証が実施されているわけではありません。ホスト認証の後には、暗号化通信を開始するための処理が入ります。というより、暗号化通信を確立する処理の途中でホスト認証が入っている、と言ったほうが正確です。

この記事では認証に着目した説明をしましたが、SSHの重要な機能として認証の他に通信の暗号化があります。暗号化のためには共通の鍵をクライアントとサーバーで共有する必要がありますが、実はその途中でホスト認証が実施されています。そして、暗号化通信が確立された後で、ユーザー認証が実施されます。

上記だけでは分かりづらいかもしれないので、RFCを引用しながら説明します。

まず、SSHは以下の3つの要素で構成されています。

  • Transport Layer Protocol
  • Authentication Protocol
  • Connection Protocol

そしてRFC 4251では、Transport Layer Protocolは以下のように説明されています。

The Transport Layer Protocol [SSH-TRANS] provides server authentication, confidentiality, and integrity.

server authenticationは読んで字の如く、サーバーの認証を意味しています。confidentialityは機密性のことで、通信の暗号化を意味しています。integrityは完全性のことで、通信内容の改ざん検知を意味しています。

また、Authentication Protocolは以下のように説明されています。

The purpose of this protocol is to perform client user authentication. It assumes that this runs over a secure transport layer protocol, which has already authenticated the server machine, established an encrypted communications channel, and computed a unique session identifier for this session.

というわけで、Transport Layer Protocolで安全な通信を確立(サーバーの認証も実施)した後で、Authentication Protocolでユーザーを認証しています。

てつてつ

ホスト認証はユーザー認証より前に実施されています。

と変更しました。(「の前」を「より前」にしただけなので、あまり変わっていませんが。)

HikaeHikae

詳細なご回答ありがとうございます。

ホスト認証からユーザー認証のシーケンスは単純ではないのですね。
(一つ一つの流れは理解できるものの、全体の流れを細かく把握しようと思うとなかなか難しい)

SSHの仕組みが気になって安易に調べ始めたものの、SSHを構成する3要素も分かっていませんでした。
暗認本を読んで基礎から学びたいと思います。

ありがとうございました!

taroimotaroimo

1点質問させてください、、!
公開鍵認証の流れの6番と7番についてです。
6番の事前に送られた公開鍵と、5番で送られてきた公開鍵を比較する部分の必要性があまりわかりません。
7番で署名の検証をすると思うのですが、事前に送られてきた公開鍵(2番で共有した公開鍵)で検証すれば署名の確認ができるので、6番はいらないし、何なら5番で公開鍵を送らなくてもいいじゃんって思ってしまうのですが、、
記事を完全に理解できておらず、トンチンカンなこと言ってたら申し訳ないです。

公開鍵認証について、調べていたらこの記事にたどり着きました。今まで「サーバー側の公開鍵で乱数作って〜」という説明で理解していたので助かりました。ありがとうございます!

てつてつ

2番は事前準備として1回だけ実施します。また1人のユーザーが複数の公開鍵をサーバーに登録しておくことも可能ですし、サーバー上に複数ユーザーが存在することだってあります。

5番・6番がないと、誰の、どの公開鍵で署名を検証したらいいのか分からなくなってしまうと思います。想像ですが、この辺りが理由ではないでしょうか。

サーバーに登録されている全てのユーザーの全ての公開鍵を片っ端から使って署名を検証するという方法も考えられますが、これだと無駄が多そうですよね。

いわもと こういちいわもと こういち

5番・6番がないと、誰の、どの公開鍵で署名を検証したらいいのか分からなくなってしまうと思います。想像ですが、この辺りが理由ではないでしょうか。

まさにこれが理由ですね。
極端な例ですが、GitHubではgitという一つのアカウントで一億を超えるユーザーを扱います。全員がSSH公開鍵を登録しているわけではないでしょうが、おそらく数千万レベルの公開鍵が登録されているので、これらの鍵全部で署名検証を試すのは現実的では無いでしょう。

また、5番で署名を省略して公開鍵だけを送る事も出来ます。この場合、対象のユーザーに対してその公開鍵が使えるかの確認を行う事が出来ます。OpenSSHではこれを利用し、使えない場合は署名の生成を行わない(秘密鍵のパスフレーズを訊かない)という動作を行います。

サーバーに登録されている全てのユーザーの全ての公開鍵を片っ端から使って署名を検証するという方法も考えられますが、これだと無駄が多そうですよね。

ユーザー認証要求にはどのユーザーでログインするかの情報も含まれているので、対象のユーザーに登録されている公開鍵だけで済みます。まあそれでも現実的では無い場合も有りますが。

てつてつ

ありがとうございます。勉強になりました。

ユーザー認証要求にはどのユーザーでログインするかの情報も含まれているので、対象のユーザーに登録されている公開鍵だけで済みます。

ここも、おっしゃる通りですね。