Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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 メッセージが送信された場合でも、クライアントが認証を続行できるようにする変更です。既存の実装では、認証要求後にサーバーから認証成功または失敗のメッセージがすぐに返されることを前提としていたため、バナーメッセージが送信されると認証が中断されていました。この変更により、noneAuthpasswordAuth、および 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メッセージを送信する可能性があります。

このコミットでは、この問題を解決するために以下の変更が行われました。

  1. 共通の応答ハンドリング関数の導入 (handleAuthResponse):

    • noneAuthpasswordAuthauth()メソッド内に重複していたパケット読み取りとメッセージタイプ判定のロジックが、新しく導入されたhandleAuthResponse関数に集約されました。
    • このhandleAuthResponse関数は、transportからパケットを読み取り、そのメッセージタイプに基づいて処理を分岐します。
    • 特に重要なのは、msgUserAuthBannerを受信した場合の処理です。この関数はバナーメッセージを認識し、それを処理(現在はTODOコメントでユーザーへの表示が示唆されているが、実際には読み飛ばす)した後、ループを継続して次のパケットを読み込みます。これにより、バナーメッセージが認証フローを中断させることなく、クライアントは期待する認証結果メッセージを待ち続けることができます。
    • msgUserAuthFailuremsgUserAuthSuccessmsgDisconnectといった他の重要なメッセージタイプもこの関数内で適切に処理されます。
  2. 公開鍵認証のロジックの改善 (publickeyAuth, validateKey, confirmKeyAck):

    • publickeyAuthauth()メソッドは、公開鍵の検証(validateKey)と実際の認証要求の2段階で動作します。
    • validateKey関数は、サーバーが特定の公開鍵を受け入れるかどうかを問い合わせるためにmsgUserAuthRequestHasSig: false)を送信します。この要求に対するサーバーの応答を処理するために、新しくconfirmKeyAck関数が導入されました。
    • confirmKeyAck関数もまた、handleAuthResponseと同様にループ内でパケットを読み取り、msgUserAuthBannerを読み飛ばし、msgUserAuthPubKeyOk(鍵が受け入れられたことを示す)またはmsgUserAuthFailure(鍵が受け入れられなかったことを示す)を待ちます。
    • これにより、公開鍵の検証段階でもバナーメッセージが適切に処理されるようになりました。
    • publickeyAuthMsg構造体の定義がauthメソッドのスコープ外に移動され、より広いスコープで利用可能になりました。これはコードの整理と可読性の向上に寄与します。

これらの変更により、GoのSSHクライアントは、SSHプロトコルの仕様に準拠し、認証プロセス中にサーバーから送信されるバナーメッセージを適切に処理できるようになり、認証の堅牢性が向上しました。

コアとなるコードの変更箇所

このコミットにおける主要な変更は、src/pkg/exp/ssh/client_auth.go ファイルに集中しています。

  1. noneAuth および passwordAuthauth() メソッドの変更:

    • これらのメソッドから、直接パケットを読み取り、msgUserAuthSuccessmsgUserAuthFailure を処理する重複したロジックが削除されました。
    • 代わりに、新しく導入された 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)
     }
    
  2. 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"`
    -	}
    
  3. 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")
     }
    
  4. 新しい関数 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メッセージを、認証フローを中断させることなく適切に処理するためのロジックの導入です。これは主に、handleAuthResponseconfirmKeyAckという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") // この行には到達しないはず
}

confirmKeyAckhandleAuthResponseと同様にforループを使用しており、公開鍵の検証中にmsgUserAuthBannerが送信されても、それを無視してmsgUserAuthPubKeyOkまたはmsgUserAuthFailureを待ち続けることができます。

これらの変更により、GoのSSHクライアントは、SSHプロトコルの柔軟なメッセージ交換に対応できるようになり、より多くのSSHサーバーとの互換性が向上しました。

関連リンク

参考にした情報源リンク