[インデックス 10592] ファイルの概要
このコミットは、Go言語の実験的なSSHパッケージである exp/ssh
内の src/pkg/exp/ssh/client_auth.go
ファイルに変更を加えています。このファイルは、SSHクライアントがリモートサーバーに対して認証を行う際のロジックを定義しています。
コミット
commit bd9dc3d55f65dce03be6d4ebbc7baaeb8e2a8964
Author: Gustav Paul <gustav.paul@gmail.com>
Date: Fri Dec 2 10:34:42 2011 -0500
exp/ssh: allow for msgUserAuthBanner during authentication
The SSH spec allows for the server to send a banner message to the client at any point during the authentication process. Currently the ssh client auth types all assume that the first response from the server after issuing a userAuthRequestMsg will be one of a couple of possible authentication success/failure messages. This means that client authentication breaks if the ssh server being connected to has a banner message configured.
This changeset refactors the noneAuth, passwordAuth and publickeyAuth types' auth() function and allows for msgUserAuthBanner during authentication.
R=golang-dev, rsc, dave, agl
CC=golang-dev
https://golang.org/cl/5432065
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/bd9dc3d55f65dce03be6d4ebbc7baaeb8e2a8964
元コミット内容
このコミットは、Go言語の実験的なSSHパッケージ (exp/ssh
) において、認証プロセス中にサーバーから msgUserAuthBanner
メッセージが送信された場合でも、クライアントが認証を続行できるようにする変更です。既存の実装では、認証要求後にサーバーから認証成功または失敗のメッセージがすぐに返されることを前提としていたため、バナーメッセージが送信されると認証が中断されていました。この変更により、noneAuth
、passwordAuth
、および publickeyAuth
の各認証タイプの auth()
関数がリファクタリングされ、msgUserAuthBanner
の受信が許容されるようになりました。
変更の背景
SSHプロトコル(RFC 4252)では、サーバーは認証プロセスのどの時点でもクライアントにバナーメッセージ(SSH_MSG_USERAUTH_BANNER
)を送信することが許可されています。このバナーメッセージは、通常、ログイン前の警告、利用規約、またはシステム情報などをユーザーに表示するために使用されます。
しかし、このコミットが作成される前のGoのexp/ssh
パッケージのクライアント認証実装では、UserAuthRequest
メッセージを送信した後、サーバーからの最初の応答が認証の成功または失敗を示すメッセージ(SSH_MSG_USERAUTH_SUCCESS
またはSSH_MSG_USERAUTH_FAILURE
)のいずれかであると仮定していました。この前提が原因で、もし接続先のSSHサーバーがバナーメッセージを送信するように設定されている場合、クライアントは予期しないmsgUserAuthBanner
メッセージを受信し、それをエラーとして処理してしまい、結果として認証プロセスが中断され、クライアントがサーバーに接続できないという問題が発生していました。
この問題に対処するため、認証プロセス中にバナーメッセージを適切に処理し、認証フローを中断させないようにするための変更が必要となりました。
前提知識の解説
SSH (Secure Shell)
SSHは、ネットワークを介して安全にコンピュータを操作するためのプロトコルです。主にリモートログイン、リモートコマンド実行、ファイル転送(SCP, SFTP)などに使用されます。SSHは、クライアントとサーバー間の通信を暗号化し、認証メカニズムを提供することで、盗聴や改ざんを防ぎます。
SSH認証
SSH認証は、クライアントがサーバーに対して自身の身元を証明するプロセスです。一般的な認証方法には以下のものがあります。
- パスワード認証 (Password Authentication): ユーザー名とパスワードを使用して認証します。
- 公開鍵認証 (Public Key Authentication): クライアントが秘密鍵を保持し、サーバーが対応する公開鍵を保持することで認証します。より安全な方法とされています。
none
認証 (None Authentication): 認証方法を試す前に、サーバーがどの認証方法をサポートしているかを確認するために使用されることがあります。
RFC 4252 (The Secure Shell (SSH) Authentication Protocol)
RFC 4252は、SSHプロトコルにおける認証プロトコルを定義する標準ドキュメントです。このドキュメントには、クライアントとサーバー間で交換される様々な認証関連のメッセージタイプが規定されています。
SSH_MSG_USERAUTH_BANNER
(msgUserAuthBanner
)
SSH_MSG_USERAUTH_BANNER
は、RFC 4252で定義されているメッセージタイプの一つです。サーバーがクライアントに対して、認証プロセス中に表示すべきテキストメッセージ(バナー)を送信するために使用されます。このメッセージは、認証の成功や失敗とは直接関係なく、情報提供のために送信されます。
Goの exp/ssh
パッケージ
exp/ssh
は、Go言語の標準ライブラリの一部として提供されている、SSHプロトコルのクライアントおよびサーバー実装の実験的なパッケージです。exp
というプレフィックスは、このパッケージがまだ開発段階であり、APIが変更される可能性があることを示しています。
技術的詳細
このコミットの技術的な核心は、SSH認証プロトコルにおけるSSH_MSG_USERAUTH_BANNER
メッセージの非同期的な性質を適切に処理することにあります。
従来のexp/ssh
クライアントの認証ロジックでは、UserAuthRequest
メッセージ(例:パスワード認証要求、公開鍵認証要求)をサーバーに送信した後、クライアントはtransport.readPacket()
を呼び出し、すぐにSSH_MSG_USERAUTH_SUCCESS
またはSSH_MSG_USERAUTH_FAILURE
のいずれかのメッセージが返されることを期待していました。しかし、SSHプロトコルの仕様では、サーバーはこれらの認証結果メッセージの前に、またはその間にSSH_MSG_USERAUTH_BANNER
メッセージを送信する可能性があります。
このコミットでは、この問題を解決するために以下の変更が行われました。
-
共通の応答ハンドリング関数の導入 (
handleAuthResponse
):noneAuth
とpasswordAuth
のauth()
メソッド内に重複していたパケット読み取りとメッセージタイプ判定のロジックが、新しく導入されたhandleAuthResponse
関数に集約されました。- この
handleAuthResponse
関数は、transport
からパケットを読み取り、そのメッセージタイプに基づいて処理を分岐します。 - 特に重要なのは、
msgUserAuthBanner
を受信した場合の処理です。この関数はバナーメッセージを認識し、それを処理(現在はTODO
コメントでユーザーへの表示が示唆されているが、実際には読み飛ばす)した後、ループを継続して次のパケットを読み込みます。これにより、バナーメッセージが認証フローを中断させることなく、クライアントは期待する認証結果メッセージを待ち続けることができます。 msgUserAuthFailure
、msgUserAuthSuccess
、msgDisconnect
といった他の重要なメッセージタイプもこの関数内で適切に処理されます。
-
公開鍵認証のロジックの改善 (
publickeyAuth
,validateKey
,confirmKeyAck
):publickeyAuth
のauth()
メソッドは、公開鍵の検証(validateKey
)と実際の認証要求の2段階で動作します。validateKey
関数は、サーバーが特定の公開鍵を受け入れるかどうかを問い合わせるためにmsgUserAuthRequest
(HasSig: false
)を送信します。この要求に対するサーバーの応答を処理するために、新しくconfirmKeyAck
関数が導入されました。confirmKeyAck
関数もまた、handleAuthResponse
と同様にループ内でパケットを読み取り、msgUserAuthBanner
を読み飛ばし、msgUserAuthPubKeyOk
(鍵が受け入れられたことを示す)またはmsgUserAuthFailure
(鍵が受け入れられなかったことを示す)を待ちます。- これにより、公開鍵の検証段階でもバナーメッセージが適切に処理されるようになりました。
publickeyAuthMsg
構造体の定義がauth
メソッドのスコープ外に移動され、より広いスコープで利用可能になりました。これはコードの整理と可読性の向上に寄与します。
これらの変更により、GoのSSHクライアントは、SSHプロトコルの仕様に準拠し、認証プロセス中にサーバーから送信されるバナーメッセージを適切に処理できるようになり、認証の堅牢性が向上しました。
コアとなるコードの変更箇所
このコミットにおける主要な変更は、src/pkg/exp/ssh/client_auth.go
ファイルに集中しています。
-
noneAuth
およびpasswordAuth
のauth()
メソッドの変更:- これらのメソッドから、直接パケットを読み取り、
msgUserAuthSuccess
やmsgUserAuthFailure
を処理する重複したロジックが削除されました。 - 代わりに、新しく導入された
handleAuthResponse(t)
関数が呼び出されるようになりました。
--- a/src/pkg/exp/ssh/client_auth.go +++ b/src/pkg/exp/ssh/client_auth.go @@ -79,19 +79,7 @@ func (n *noneAuth) auth(session []byte, user string, t *transport, rand io.Reade return false, nil, err } - packet, err := t.readPacket() - if err != nil { - return false, nil, err - } - - switch packet[0] { - case msgUserAuthSuccess: - return true, nil, nil - case msgUserAuthFailure: - msg := decode(packet).(*userAuthFailureMsg) - return false, msg.Methods, nil - } - return false, nil, UnexpectedMessageError{msgUserAuthSuccess, packet[0]} + return handleAuthResponse(t) } func (n *noneAuth) method() string { @@ -127,19 +115,7 @@ func (p *passwordAuth) auth(session []byte, user string, t *transport, rand io.R return false, nil, err } - packet, err := t.readPacket() - if err != nil { - return false, nil, err - } - - switch packet[0] { - case msgUserAuthSuccess: - return true, nil, nil - case msgUserAuthFailure: - msg := decode(packet).(*userAuthFailureMsg) - return false, msg.Methods, nil - } - return false, nil, UnexpectedMessageError{msgUserAuthSuccess, packet[0]} + return handleAuthResponse(t) }
- これらのメソッドから、直接パケットを読み取り、
-
publickeyAuthMsg
構造体の移動:publickeyAuthMsg
構造体の定義がpublickeyAuth.auth()
メソッド内から、ファイル内のよりグローバルなスコープに移動されました。
--- a/src/pkg/exp/ssh/client_auth.go +++ b/src/pkg/exp/ssh/client_auth.go @@ -173,27 +149,28 @@ type publickeyAuth struct { ClientKeyring } +type publickeyAuthMsg struct { + User string + Service string + Method string + // HasSig indicates to the reciver packet that the auth request is signed and + // should be used for authentication of the request. + HasSig bool + Algoname string + Pubkey string + // Sig is defined as []byte so marshal will exclude it during validateKey + Sig []byte `ssh:"rest"` +} + func (p *publickeyAuth) auth(session []byte, user string, t *transport, rand io.Reader) (bool, []string, error) { - type publickeyAuthMsg struct { - User string - Service string - Method string - // HasSig indicates to the reciver packet that the auth request is signed and - // should be used for authentication of the request. - HasSig bool - Algoname string - Pubkey string - // Sig is defined as []byte so marshal will exclude it during the query phase - Sig []byte `ssh:"rest"` - }
-
publickeyAuth.auth()
メソッドの変更と新しいヘルパー関数の導入:- 公開鍵の検証フェーズで、
validateKey
関数が導入され、その中でconfirmKeyAck
が呼び出されるようになりました。 - 実際の認証要求後も、
handleAuthResponse
が呼び出されるようになりました。
--- a/src/pkg/exp/ssh/client_auth.go +++ b/src/pkg/exp/ssh/client_auth.go @@ -204,33 +181,13 @@ func (p *publickeyAuth) auth(session []byte, user string, t *transport, rand io. } - pubkey := serializePublickey(key) - algoname := algoName(key) - msg := publickeyAuthMsg{ - User: user, - Service: serviceSSH, - Method: p.method(), - HasSig: false, - Algoname: algoname, - Pubkey: string(pubkey), - } - if err := t.writePacket(marshal(msgUserAuthRequest, msg)); err != nil { - return false, nil, err - } - packet, err := t.readPacket() - if err != nil { - return false, nil, err - } - switch packet[0] { - case msgUserAuthPubKeyOk: - msg := decode(packet).(*userAuthPubKeyOkMsg) - if msg.Algo != algoname || msg.PubKey != string(pubkey) { - continue - } + if ok, err := p.validateKey(key, user, t); ok { validKeys[index] = key - case msgUserAuthFailure: - default: - return false, nil, UnexpectedMessageError{msgUserAuthSuccess, packet[0]} + } else { + if err != nil { + return false, nil, err + } } index++ } @@ -265,24 +222,61 @@ func (p *publickeyAuth) auth(session []byte, user string, t *transport, rand io. if err := t.writePacket(p); err != nil { return false, nil, err }\ - packet, err := t.readPacket() + success, methods, err := handleAuthResponse(t) if err != nil { return false, nil, err } - switch packet[0] { - case msgUserAuthSuccess: - return true, nil, nil + if success { + return success, methods, err + } + } + return false, methods, nil +} + +// validateKey validates the key provided it is acceptable to the server. +func (p *publickeyAuth) validateKey(key interface{}, user string, t *transport) (bool, error) { + pubkey := serializePublickey(key) + algoname := algoName(key) + msg := publickeyAuthMsg{ + User: user, + Service: serviceSSH, + Method: p.method(), + HasSig: false, + Algoname: algoname, + Pubkey: string(pubkey), + } + if err := t.writePacket(marshal(msgUserAuthRequest, msg)); err != nil { + return false, err + } + + return p.confirmKeyAck(key, t) +} + +func (p *publickeyAuth) confirmKeyAck(key interface{}, t *transport) (bool, error) { + pubkey := serializePublickey(key) + algoname := algoName(key) + + for { + packet, err := t.readPacket() + if err != nil { + return false, err + } + switch packet[0] { + case msgUserAuthBanner: + // TODO(gpaul): add callback to present the banner to the user + case msgUserAuthPubKeyOk: + msg := decode(packet).(*userAuthPubKeyOkMsg) + if msg.Algo != algoname || msg.PubKey != string(pubkey) { + return false, nil + } + return true, nil case msgUserAuthFailure: - msg := decode(packet).(*userAuthFailureMsg) - methods = msg.Methods - continue - case msgDisconnect: - return false, nil, io.EOF + return false, nil default: - return false, nil, UnexpectedMessageError{msgUserAuthSuccess, packet[0]} + return false, UnexpectedMessageError{msgUserAuthSuccess, packet[0]} } } - return false, methods, nil + panic("unreachable") }
- 公開鍵の検証フェーズで、
-
新しい関数
handleAuthResponse
の追加:- 認証応答を処理するための新しいヘルパー関数がファイルの下部に追加されました。
--- a/src/pkg/exp/ssh/client_auth.go +++ b/src/pkg/exp/ssh/client_auth.go @@ -293,3 +287,30 @@ func (p *publickeyAuth) method() string { func ClientAuthPublickey(impl ClientKeyring) ClientAuth { return &publickeyAuth{impl} } + +// handleAuthResponse returns whether the preceding authentication request succeeded +// along with a list of remaining authentication methods to try next and +// an error if an unexpected response was received. +func handleAuthResponse(t *transport) (bool, []string, error) { + for { + packet, err := t.readPacket() + if err != nil { + return false, nil, err + } + + switch packet[0] { + case msgUserAuthBanner: + // TODO: add callback to present the banner to the user + case msgUserAuthFailure: + msg := decode(packet).(*userAuthFailureMsg) + return false, msg.Methods, nil + case msgUserAuthSuccess: + return true, nil, nil + case msgDisconnect: + return false, nil, io.EOF + default: + return false, nil, UnexpectedMessageError{msgUserAuthSuccess, packet[0]} + } + } + panic("unreachable") +}
コアとなるコードの解説
このコミットの核心は、SSH認証プロセス中にサーバーから送信される可能性のあるmsgUserAuthBanner
メッセージを、認証フローを中断させることなく適切に処理するためのロジックの導入です。これは主に、handleAuthResponse
とconfirmKeyAck
という2つの新しいヘルパー関数によって実現されています。
handleAuthResponse
関数
この関数は、SSHクライアントが認証要求を送信した後にサーバーから受信するパケットを処理するための汎用的なメカニズムを提供します。
func handleAuthResponse(t *transport) (bool, []string, error) {
for { // 無限ループでパケットを読み続ける
packet, err := t.readPacket() // サーバーから次のパケットを読み込む
if err != nil {
return false, nil, err // 読み込みエラーが発生した場合はエラーを返す
}
switch packet[0] { // パケットの最初のバイト(メッセージタイプ)に基づいて処理を分岐
case msgUserAuthBanner:
// TODO: add callback to present the banner to the user
// バナーメッセージを受信した場合、現在は単に読み飛ばす。
// 将来的には、このバナーの内容をユーザーに表示するためのコールバックを追加する予定。
// このメッセージは認証の成功/失敗とは関係ないので、ループを継続して次のメッセージを待つ。
case msgUserAuthFailure:
// 認証失敗メッセージを受信した場合
msg := decode(packet).(*userAuthFailureMsg)
return false, msg.Methods, nil // 認証失敗を返し、サーバーが提案する代替認証方法のリストを返す
case msgUserAuthSuccess:
// 認証成功メッセージを受信した場合
return true, nil, nil // 認証成功を返す
case msgDisconnect:
// サーバーからの切断メッセージを受信した場合
return false, nil, io.EOF // EOFエラーを返す
default:
// 予期しないメッセージタイプを受信した場合
return false, nil, UnexpectedMessageError{msgUserAuthSuccess, packet[0]} // エラーを返す
}
}
panic("unreachable") // この行には到達しないはず(ループ内で常にreturnされるため)
}
handleAuthResponse
の重要な点は、for
ループを使用していることです。これにより、msgUserAuthBanner
のような情報提供のためのメッセージが認証結果メッセージの前に送信された場合でも、クライアントはそれらを読み飛ばし、期待する認証成功/失敗メッセージが到着するまでパケットの読み込みを継続できます。これにより、バナーメッセージによって認証フローが中断されることがなくなります。
confirmKeyAck
関数
この関数は、公開鍵認証の初期段階(鍵の検証フェーズ)において、サーバーからの応答を処理するために特化して導入されました。
func (p *publickeyAuth) confirmKeyAck(key interface{}, t *transport) (bool, error) {
pubkey := serializePublickey(key)
algoname := algoName(key)
for { // 無限ループでパケットを読み続ける
packet, err := t.readPacket() // サーバーから次のパケットを読み込む
if err != nil {
return false, err // 読み込みエラーが発生した場合はエラーを返す
}
switch packet[0] { // パケットの最初のバイト(メッセージタイプ)に基づいて処理を分岐
case msgUserAuthBanner:
// TODO(gpaul): add callback to present the banner to the user
// ここでもバナーメッセージを読み飛ばし、ループを継続する。
case msgUserAuthPubKeyOk:
// 公開鍵がサーバーに受け入れられたことを示すメッセージを受信した場合
msg := decode(packet).(*userAuthPubKeyOkMsg)
// 受信した鍵情報が送信したものと一致するか検証
if msg.Algo != algoname || msg.PubKey != string(pubkey) {
return false, nil // 不一致の場合は失敗を返す
}
return true, nil // 鍵が受け入れられたことを示す
case msgUserAuthFailure:
// 公開鍵がサーバーに受け入れられなかったことを示すメッセージを受信した場合
return false, nil // 失敗を返す
default:
// 予期しないメッセージタイプを受信した場合
return false, UnexpectedMessageError{msgUserAuthSuccess, packet[0]} // エラーを返す
}
}
panic("unreachable") // この行には到達しないはず
}
confirmKeyAck
もhandleAuthResponse
と同様にfor
ループを使用しており、公開鍵の検証中にmsgUserAuthBanner
が送信されても、それを無視してmsgUserAuthPubKeyOk
またはmsgUserAuthFailure
を待ち続けることができます。
これらの変更により、GoのSSHクライアントは、SSHプロトコルの柔軟なメッセージ交換に対応できるようになり、より多くのSSHサーバーとの互換性が向上しました。
関連リンク
- RFC 4252 - The Secure Shell (SSH) Authentication Protocol: https://www.rfc-editor.org/rfc/rfc4252
参考にした情報源リンク
- GitHubコミットページ: https://github.com/golang/go/commit/bd9dc3d55f65dce03be6d4ebbc7baaeb8e2a8964
- Gerrit Change-ID: https://golang.org/cl/5432065