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

[インデックス 10273] ファイルの概要

このコミットは、Go言語の実験的なSSHパッケージ exp/ssh におけるクライアント認証のサポートを改善するものです。具体的には、SSH認証の様々なメソッドを扱うための新しいAPIが導入されました。この変更により、以前は ClientConfig 構造体の Password フィールドに直接パスワードを設定する形だったものが、Auth フィールドに ClientAuth インターフェースを実装した認証メソッドのリストを設定する形に拡張されました。これにより、将来的に他の認証方法(公開鍵認証など)を容易に追加できるような設計になっています。

コミット

commit 1170a6460f3917f0b060f6de654759edb98f3df5
Author: Dave Cheney <dave@cheney.net>
Date:   Mon Nov 7 12:37:05 2011 -0500

    exp/ssh: improved client authentication support
    
    This CL adds an API for handling the various SSH
    authenticaton methods. None and password continue
    to be the only supported methods.
    
    R=bradfitz, agl, n13m3y3r, rsc, cw
    CC=golang-dev
    https://golang.org/cl/5328045

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/1170a6460f3917f0b060f6de654759edb98f3df5

元コミット内容

exp/ssh: improved client authentication support

この変更は、様々なSSH認証メソッドを扱うためのAPIを追加します。現在サポートされているのは「none」と「password」メソッドのみです。

変更の背景

このコミット以前の exp/ssh パッケージでは、クライアント認証は非常に限定的で、ClientConfig 構造体の Password フィールドに直接パスワードを設定する形式でした。これは、SSHがサポートする多様な認証方法(公開鍵認証、GSSAPI認証など)に対応するには不十分な設計でした。

この変更の背景には、SSHプロトコルが提供する柔軟な認証メカニズムをGoのSSHクライアントでも利用できるようにするという意図があります。特に、RFC 4252で定義されている「none」認証(サーバーがサポートする認証方法を問い合わせるために使用される)や「password」認証以外の認証方法を将来的に追加することを視野に入れ、より拡張性の高い認証フレームワークを導入する必要がありました。

これにより、ユーザーは単一のパスワードだけでなく、複数の認証方法を組み合わせて試行したり、より複雑な認証ロジックを実装したりすることが可能になります。

前提知識の解説

SSH (Secure Shell)

SSHは、ネットワークを介してコンピュータを安全に操作するためのプロトコルです。主にリモートログインやファイル転送(SCP, SFTP)に利用されます。SSHは、クライアントとサーバー間で暗号化された通信チャネルを確立し、データの盗聴や改ざんを防ぎます。

SSH認証プロトコル (RFC 4252)

SSHプロトコルスイートの一部であり、クライアントがサーバーに対して自身を認証する方法を定義しています。RFC 4252では、以下のような様々な認証方法が規定されています。

  • none 認証: クライアントが認証情報を提供せずに認証を試みる方法です。主にサーバーがサポートする認証方法のリストを取得するために使用されます。サーバーが none 認証を許可することは稀ですが、認証失敗時に利用可能な認証方法のリストをクライアントに返すのが一般的です。
  • password 認証: ユーザー名とパスワードを使用して認証を行う最も一般的な方法です。
  • publickey 認証: クライアントが秘密鍵を保持し、対応する公開鍵がサーバーに登録されている場合に、チャレンジ&レスポンス方式で認証を行う方法です。パスワード認証よりも安全性が高いとされています。
  • hostbased 認証: クライアントが接続元のホストの身元を証明することで認証を行う方法です。
  • gssapi-with-mic 認証: KerberosなどのGSSAPI(Generic Security Service Application Program Interface)を利用した認証方法です。

Go言語の exp/ssh パッケージ

exp/ssh は、Go言語の標準ライブラリの一部として提供されていた実験的なSSHパッケージです。exp は "experimental" の略で、将来的に標準ライブラリに統合される可能性のある、あるいは新しい機能やAPIを試すためのパッケージであることを示しています。このパッケージは、SSHクライアントとサーバーの実装を提供し、GoアプリケーションでSSH通信を行うための基盤となります。

インターフェース (Go言語)

Go言語におけるインターフェースは、メソッドのシグネチャの集まりを定義する型です。特定のインターフェースを実装する型は、そのインターフェースが定義するすべてのメソッドを持っている必要があります。これにより、異なる具体的な型が同じインターフェースを実装することで、ポリモーフィックな振る舞いを実現できます。このコミットでは、ClientAuth インターフェースが導入され、様々な認証方法を統一的に扱うための抽象化が提供されています。

技術的詳細

このコミットの主要な変更点は、SSHクライアント認証のメカニズムを、固定的なパスワード認証から、より柔軟なプラグイン可能な認証方法のフレームワークへと移行したことです。

  1. ClientAuth インターフェースの導入:

    • ClientAuth インターフェースは、SSH認証メソッドのインスタンスを表します。
    • このインターフェースは2つのメソッドを定義します:
      • auth(user string, t *transport) (bool, []string, error): ユーザー認証を試行します。認証が成功した場合は true を返し、失敗した場合はサーバーが提示する代替認証メソッドのリストを返します。
      • method() string: RFC 4252で定義されている認証メソッド名(例: "none", "password")を返します。
    • これにより、異なる認証方法(例: noneAuth, passwordAuth)がこのインターフェースを実装することで、統一された方法で認証処理を呼び出すことが可能になります。
  2. ClientConfig 構造体の変更:

    • 以前は Password string フィールドでパスワードを直接保持していましたが、これが削除されました。
    • 代わりに Auth []ClientAuth フィールドが追加されました。これは、クライアントが試行する認証メソッドのリストを保持します。これにより、複数の認証方法を順番に試すことが可能になります。
  3. authenticate メソッドのリファクタリング:

    • ClientConnauthenticate メソッドが大幅にリファクタリングされました。
    • 新しい authenticate メソッドは、まず「none」認証を試行します。
    • 「none」認証が失敗し、サーバーが利用可能な認証メソッドのリストを返した場合、クライアントは ClientConfig.Auth に設定された認証メソッドの中から、まだ試行しておらず、かつサーバーがサポートしているメソッドを順番に試行します。
    • このプロセスは、認証が成功するか、利用可能な認証メソッドがなくなるまで繰り返されます。
  4. noneAuthpasswordAuth の実装:

    • noneAuthClientAuth インターフェースを実装し、RFC 4252の「none」認証を処理します。
    • passwordAuthClientAuth インターフェースを実装し、RFC 4252の「password」認証を処理します。
    • passwordAuthClientPassword インターフェース(Password(user string) (password string, err error) メソッドを持つ)を利用してパスワードを取得するようになり、パスワードの取得方法を抽象化しています。これにより、パスワードを直接コードに埋め込むのではなく、外部から提供されるように設計されています。

この変更により、SSHクライアントはサーバーとの認証ネゴシエーションをより適切に行い、複数の認証方法を柔軟に試行できるようになりました。

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

src/pkg/exp/ssh/Makefile

--- a/src/pkg/exp/ssh/Makefile
+++ b/src/pkg/exp/ssh/Makefile
@@ -8,6 +8,7 @@ TARG=exp/ssh
 GOFILES=\
 	channel.go\
 	client.go\
+\tclient_auth.go\
 	common.go\
 	messages.go\
 	transport.go\

client_auth.go が新しく追加されたため、Makefile にそのファイルがビルド対象として追加されています。

src/pkg/exp/ssh/client.go

--- a/src/pkg/exp/ssh/client.go
+++ b/src/pkg/exp/ssh/client.go
@@ -131,56 +131,6 @@ func (c *ClientConn) handshake() error {
 	return c.transport.reader.setupKeys(serverKeys, K, H, H, hashFunc)
 }
 
-// authenticate authenticates with the remote server. See RFC 4252. 
-// Only "password" authentication is supported.
-func (c *ClientConn) authenticate() error {
-	if err := c.writePacket(marshal(msgServiceRequest, serviceUserAuth)); err != nil {
-		return err
-	}
-	packet, err := c.readPacket()
-	if err != nil {
-		return err
-	}
-
-	var serviceAccept serviceAcceptMsg
-	if err = unmarshal(&serviceAccept, packet, msgServiceAccept); err != nil {
-		return err
-		return err
-	}
-
-	// TODO(dfc) support proper authentication method negotation
-	method := "none"
-	if c.config.Password != "" {
-		method = "password"
-	}
-	if err := c.sendUserAuthReq(method); err != nil {
-		return err
-	}
-
-	if packet, err = c.readPacket(); err != nil {
-		return err
-	}
-
-	if packet[0] != msgUserAuthSuccess {
-		return UnexpectedMessageError{msgUserAuthSuccess, packet[0]}
-	}
-	return nil
-}
-
-func (c *ClientConn) sendUserAuthReq(method string) error {
-	length := stringLength([]byte(c.config.Password)) + 1
-	payload := make([]byte, length)
-	// always false for password auth, see RFC 4252 Section 8.
-	payload[0] = 0
-	marshalString(payload[1:], []byte(c.config.Password))
-
-	return c.writePacket(marshal(msgUserAuthRequest, userAuthRequestMsg{
-		User:    c.config.User,
-		Service: serviceSSH,
-		Method:  method,
-		Payload: payload,
-	}))
-}
-
 // kexDH performs Diffie-Hellman key agreement on a ClientConn. The
 // returned values are given the same names as in RFC 4253, section 8.
 func (c *ClientConn) kexDH(group *dhGroup, hashFunc crypto.Hash, magics *handshakeMagics, hostKeyAlgo string) ([]byte, []byte, error) {
@@ -348,8 +298,9 @@ type ClientConfig struct {
 	// The username to authenticate.
 	User string
 
-	// Used for "password" method authentication.
-	Password string
+	// A slice of ClientAuth methods. Only the first instance 
+	// of a particular RFC 4252 method will be used during authentication.
+	Auth []ClientAuth
 }
 
 func (c *ClientConfig) rand() io.Reader {

authenticate 関数と sendUserAuthReq 関数が削除され、認証ロジックが client_auth.go に移動しました。 ClientConfig 構造体から Password フィールドが削除され、代わりに Auth []ClientAuth フィールドが追加されました。

src/pkg/exp/ssh/client_auth.go (新規ファイル)

--- /dev/null
+++ b/src/pkg/exp/ssh/client_auth.go
@@ -0,0 +1,157 @@
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package ssh
+
+import (
+	"errors"
+)
+
+// authenticate authenticates with the remote server. See RFC 4252. 
+func (c *ClientConn) authenticate() error {
+	// initiate user auth session
+	if err := c.writePacket(marshal(msgServiceRequest, serviceUserAuth)); err != nil {
+		return err
+	}
+	packet, err := c.readPacket()
+	if err != nil {
+		return err
+	}
+	var serviceAccept serviceAcceptMsg
+	if err := unmarshal(&serviceAccept, packet, msgServiceAccept); err != nil {
+		return err
+	}
+	// during the authentication phase the client first attempts the "none" method
+	// then any untried methods suggested by the server. 
+	tried, remain := make(map[string]bool), make(map[string]bool)
+	for auth := ClientAuth(new(noneAuth)); auth != nil; {
+		ok, methods, err := auth.auth(c.config.User, c.transport)
+		if err != nil {
+			return err
+		}
+		if ok {
+			// success
+			return nil
+		}
+		tried[auth.method()] = true
+		delete(remain, auth.method())
+		for _, meth := range methods {
+			if tried[meth] {
+				// if we've tried meth already, skip it.
+				continue
+			}
+			remain[meth] = true
+		}
+		auth = nil
+		for _, a := range c.config.Auth {
+			if remain[a.method()] {
+				auth = a
+				break
+			}
+		}
+	}
+	return errors.New("ssh: unable to authenticate, no supported methods remain")
+}
+
+// A ClientAuth represents an instance of an RFC 4252 authentication method.
+type ClientAuth interface {
+	// auth authenticates user over transport t. 
+	// Returns true if authentication is successful.
+	// If authentication is not successful, a []string of alternative 
+	// method names is returned.
+	auth(user string, t *transport) (bool, []string, error)
+
+	// method returns the RFC 4252 method name.
+	method() string
+}
+
+// "none" authentication, RFC 4252 section 5.2.
+type noneAuth int
+
+func (n *noneAuth) auth(user string, t *transport) (bool, []string, error) {
+	if err := t.writePacket(marshal(msgUserAuthRequest, userAuthRequestMsg{
+		User:    user,
+		Service: serviceSSH,
+		Method:  "none",
+	})); err != nil {
+		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]}
+}
+
+func (n *noneAuth) method() string {
+	return "none"
+}
+
+// "password" authentication, RFC 4252 Section 8.
+type passwordAuth struct {
+	ClientPassword
+}
+
+func (p *passwordAuth) auth(user string, t *transport) (bool, []string, error) {
+	type passwordAuthMsg struct {
+		User     string
+		Service  string
+		Method   string
+		Reply    bool
+		Password string
+	}
+
+	pw, err := p.Password(user)
+	if err != nil {
+		return false, nil, err
+	}
+
+	if err := t.writePacket(marshal(msgUserAuthRequest, passwordAuthMsg{
+		User:     user,
+		Service:  serviceSSH,
+		Method:   "password",
+		Reply:    false,
+		Password: pw,
+	})); err != nil {
+		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]}
+}
+
+func (p *passwordAuth) method() string {
+	return "password"
+}
+
+// A ClientPassword implements access to a client's passwords.
+type ClientPassword interface {
+	// Password returns the password to use for user.
+	Password(user string) (password string, err error)
+}
+
+// ClientAuthPassword returns a ClientAuth using password authentication.
+func ClientAuthPassword(impl ClientPassword) ClientAuth {
+	return &passwordAuth{impl}
+}

このファイルは、新しい認証フレームワークの核心部分です。

  • authenticate 関数がここに移動し、認証ネゴシエーションロジックが実装されました。
  • ClientAuth インターフェースが定義されました。
  • noneAuthpasswordAuth という ClientAuth インターフェースの実装が提供されました。
  • ClientPassword インターフェースと ClientAuthPassword ヘルパー関数が追加されました。

src/pkg/exp/ssh/doc.go

--- a/src/pkg/exp/ssh/doc.go
+++ b/src/pkg/exp/ssh/doc.go
@@ -83,7 +83,7 @@ authentication method is supported.
 
 	config := &ClientConfig{
 		User: "username",
-		Password: "123456",
+		Auth: []ClientAuth{ ... },
 	}
 	client, err := Dial("yourserver.com:22", config)
 

ドキュメントの例が更新され、ClientConfigPassword フィールドの代わりに Auth フィールドを使用する新しい認証方法が示されています。

コアとなるコードの解説

ClientAuth インターフェース

type ClientAuth interface {
	auth(user string, t *transport) (bool, []string, error)
	method() string
}

このインターフェースは、SSH認証メソッドの抽象化を提供します。auth メソッドは実際の認証ロジックを実行し、成功したかどうか、およびサーバーが提示する代替メソッドのリストを返します。method メソッドは、その認証方法のRFC 4252で定義された名前を返します。

authenticate 関数 (in client_auth.go)

func (c *ClientConn) authenticate() error {
	// ... (service request/accept logic) ...

	tried, remain := make(map[string]bool), make(map[string]bool)
	for auth := ClientAuth(new(noneAuth)); auth != nil; {
		ok, methods, err := auth.auth(c.config.User, c.transport)
		if err != nil {
			return err
		}
		if ok {
			return nil // Authentication successful
		}
		tried[auth.method()] = true
		delete(remain, auth.method())
		for _, meth := range methods {
			if tried[meth] {
				continue
			}
			remain[meth] = true
		}
		auth = nil
		for _, a := range c.config.Auth {
			if remain[a.method()] {
				auth = a
				break
			}
		}
	}
	return errors.New("ssh: unable to authenticate, no supported methods remain")
}

この関数は、SSHクライアント認証の主要なロジックをカプセル化しています。

  1. まず、SSHユーザー認証サービスを要求します。
  2. 次に、noneAuth メソッドから認証試行を開始します。これは、サーバーがサポートする認証方法のリストを取得するためによく使われます。
  3. 認証が成功しなかった場合、サーバーから返された利用可能な認証メソッドのリスト (methods) を確認します。
  4. c.config.Auth に設定された認証メソッドのリストを繰り返し処理し、まだ試行しておらず、かつサーバーがサポートしているメソッドを次に試行します。
  5. このループは、認証が成功するか、試行できる認証メソッドがなくなるまで続きます。

noneAuthpasswordAuth の実装

これらの構造体は ClientAuth インターフェースを実装し、それぞれの認証方法の具体的なロジックを提供します。passwordAuthClientPassword インターフェースを介してパスワードを取得するため、パスワードの供給源を柔軟に設定できます。

ClientConfig の変更

type ClientConfig struct {
	User string
	Auth []ClientAuth // New field
}

Auth フィールドは、クライアントが認証時に試行する ClientAuth インターフェースを実装したオブジェクトのリストを保持します。これにより、ユーザーは複数の認証方法(例: まず公開鍵、次にパスワード)を定義し、SSHクライアントがそれらを順番に試行するように設定できます。

関連リンク

参考にした情報源リンク

  • RFC 4252 - The Secure Shell (SSH) Authentication Protocol
  • Go言語のドキュメントとソースコード
  • SSHプロトコルに関する一般的な知識
  • Go言語のインターフェースに関する知識
  • golang.org/x/crypto/ssh パッケージの現在の実装 (参考として)

[インデックス 10273] ファイルの概要

このコミットは、Go言語の実験的なSSHパッケージ exp/ssh におけるクライアント認証のサポートを改善するものです。具体的には、SSH認証の様々なメソッドを扱うための新しいAPIが導入されました。この変更により、以前は ClientConfig 構造体の Password フィールドに直接パスワードを設定する形だったものが、Auth フィールドに ClientAuth インターフェースを実装した認証メソッドのリストを設定する形に拡張されました。これにより、将来的に他の認証方法(公開鍵認証など)を容易に追加できるような設計になっています。

コミット

commit 1170a6460f3917f0b060f6de654759edb98f3df5
Author: Dave Cheney <dave@cheney.net>
Date:   Mon Nov 7 12:37:05 2011 -0500

    exp/ssh: improved client authentication support
    
    This CL adds an API for handling the various SSH
    authenticaton methods. None and password continue
    to be the only supported methods.
    
    R=bradfitz, agl, n13m3y3r, rsc, cw
    CC=golang-dev
    https://golang.org/cl/5328045

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/1170a6460f3917f0b060f6de654759edb98f3df5

元コミット内容

exp/ssh: improved client authentication support

この変更は、様々なSSH認証メソッドを扱うためのAPIを追加します。現在サポートされているのは「none」と「password」メソッドのみです。

変更の背景

このコミット以前の exp/ssh パッケージでは、クライアント認証は非常に限定的で、ClientConfig 構造体の Password フィールドに直接パスワードを設定する形式でした。これは、SSHがサポートする多様な認証方法(公開鍵認証、GSSAPI認証など)に対応するには不十分な設計でした。

この変更の背景には、SSHプロトコルが提供する柔軟な認証メカニズムをGoのSSHクライアントでも利用できるようにするという意図があります。特に、RFC 4252で定義されている「none」認証(サーバーがサポートする認証方法を問い合わせるために使用される)や「password」認証以外の認証方法を将来的に追加することを視野に入れ、より拡張性の高い認証フレームワークを導入する必要がありました。

これにより、ユーザーは単一のパスワードだけでなく、複数の認証方法を組み合わせて試行したり、より複雑な認証ロジックを実装したりすることが可能になります。

前提知識の解説

SSH (Secure Shell)

SSHは、ネットワークを介してコンピュータを安全に操作するためのプロトコルです。主にリモートログインやファイル転送(SCP, SFTP)に利用されます。SSHは、クライアントとサーバー間で暗号化された通信チャネルを確立し、データの盗聴や改ざんを防ぎます。

SSH認証プロトコル (RFC 4252)

SSHプロトコルスイートの一部であり、クライアントがサーバーに対して自身を認証する方法を定義しています。RFC 4252では、以下のような様々な認証方法が規定されています。

  • none 認証: クライアントが認証情報を提供せずに認証を試みる方法です。主にサーバーがサポートする認証方法のリストを取得するために使用されます。サーバーが none 認証を許可することは稀ですが、認証失敗時に利用可能な認証方法のリストをクライアントに返すのが一般的です。
  • password 認証: ユーザー名とパスワードを使用して認証を行う最も一般的な方法です。
  • publickey 認証: クライアントが秘密鍵を保持し、対応する公開鍵がサーバーに登録されている場合に、チャレンジ&レスポンス方式で認証を行う方法です。パスワード認証よりも安全性が高いとされています。
  • hostbased 認証: クライアントが接続元のホストの身元を証明することで認証を行う方法です。
  • gssapi-with-mic 認証: KerberosなどのGSSAPI(Generic Security Service Application Program Interface)を利用した認証方法です。

Go言語の exp/ssh パッケージ

exp/ssh は、Go言語の標準ライブラリの一部として提供されていた実験的なSSHパッケージです。exp は "experimental" の略で、将来的に標準ライブラリに統合される可能性のある、あるいは新しい機能やAPIを試すためのパッケージであることを示しています。このパッケージは、SSHクライアントとサーバーの実装を提供し、GoアプリケーションでSSH通信を行うための基盤となります。現在のGo言語では、golang.org/x/crypto/ssh がSSHクライアントおよびサーバーの実装に推奨されるパッケージとなっています。

インターフェース (Go言語)

Go言語におけるインターフェースは、メソッドのシグネチャの集まりを定義する型です。特定のインターフェースを実装する型は、そのインターフェースが定義するすべてのメソッドを持っている必要があります。これにより、異なる具体的な型が同じインターフェースを実装することで、ポリモーフィックな振る舞いを実現できます。このコミットでは、ClientAuth インターフェースが導入され、様々な認証方法を統一的に扱うための抽象化が提供されています。

技術的詳細

このコミットの主要な変更点は、SSHクライアント認証のメカニズムを、固定的なパスワード認証から、より柔軟なプラグイン可能な認証方法のフレームワークへと移行したことです。

  1. ClientAuth インターフェースの導入:

    • ClientAuth インターフェースは、SSH認証メソッドのインスタンスを表します。
    • このインターフェースは2つのメソッドを定義します:
      • auth(user string, t *transport) (bool, []string, error): ユーザー認証を試行します。認証が成功した場合は true を返し、失敗した場合はサーバーが提示する代替認証メソッドのリストを返します。
      • method() string: RFC 4252で定義されている認証メソッド名(例: "none", "password")を返します。
    • これにより、異なる認証方法(例: noneAuth, passwordAuth)がこのインターフェースを実装することで、統一された方法で認証処理を呼び出すことが可能になります。
  2. ClientConfig 構造体の変更:

    • 以前は Password string フィールドでパスワードを直接保持していましたが、これが削除されました。
    • 代わりに Auth []ClientAuth フィールドが追加されました。これは、クライアントが試行する認証メソッドのリストを保持します。これにより、複数の認証方法を順番に試すことが可能になります。
  3. authenticate メソッドのリファクタリング:

    • ClientConnauthenticate メソッドが大幅にリファクタリングされました。
    • 新しい authenticate メソッドは、まず「none」認証を試行します。
    • 「none」認証が失敗し、サーバーが利用可能な認証メソッドのリストを返した場合、クライアントは ClientConfig.Auth に設定された認証メソッドの中から、まだ試行しておらず、かつサーバーがサポートしているメソッドを順番に試行します。
    • このプロセスは、認証が成功するか、利用可能な認証メソッドがなくなるまで繰り返されます。
  4. noneAuthpasswordAuth の実装:

    • noneAuthClientAuth インターフェースを実装し、RFC 4252の「none」認証を処理します。
    • passwordAuthClientAuth インターフェースを実装し、RFC 4252の「password」認証を処理します。
    • passwordAuthClientPassword インターフェース(Password(user string) (password string, err error) メソッドを持つ)を利用してパスワードを取得するようになり、パスワードの取得方法を抽象化しています。これにより、パスワードを直接コードに埋め込むのではなく、外部から提供されるように設計されています。

この変更により、SSHクライアントはサーバーとの認証ネゴシエーションをより適切に行い、複数の認証方法を柔軟に試行できるようになりました。

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

src/pkg/exp/ssh/Makefile

--- a/src/pkg/exp/ssh/Makefile
+++ b/src/pkg/exp/ssh/Makefile
@@ -8,6 +8,7 @@ TARG=exp/ssh
 GOFILES=\
 	channel.go\
 	client.go\
+\tclient_auth.go\
 	common.go\
 	messages.go\
 	transport.go\

client_auth.go が新しく追加されたため、Makefile にそのファイルがビルド対象として追加されています。

src/pkg/exp/ssh/client.go

--- a/src/pkg/exp/ssh/client.go
+++ b/src/pkg/exp/ssh/client.go
@@ -131,56 +131,6 @@ func (c *ClientConn) handshake() error {
 	return c.transport.reader.setupKeys(serverKeys, K, H, H, hashFunc)
 }
 
-// authenticate authenticates with the remote server. See RFC 4252. 
-// Only "password" authentication is supported.
-func (c *ClientConn) authenticate() error {
-	if err := c.writePacket(marshal(msgServiceRequest, serviceUserAuth)); err != nil {
-		return err
-	}
-	packet, err := c.readPacket()
-	if err != nil {
-		return err
-	}
-
-	var serviceAccept serviceAcceptMsg
-	if err = unmarshal(&serviceAccept, packet, msgServiceAccept); err != nil {
-		return err
-		return err
-	}
-
-	// TODO(dfc) support proper authentication method negotation
-	method := "none"
-	if c.config.Password != "" {
-		method = "password"
-	}
-	if err := c.sendUserAuthReq(method); err != nil {
-		return err
-	}
-
-	if packet, err = c.readPacket(); err != nil {
-		return err
-	}
-
-	if packet[0] != msgUserAuthSuccess {
-		return UnexpectedMessageError{msgUserAuthSuccess, packet[0]}
-	}
-	return nil
-}
-
-func (c *ClientConn) sendUserAuthReq(method string) error {
-	length := stringLength([]byte(c.config.Password)) + 1
-	payload := make([]byte, length)
-	// always false for password auth, see RFC 4252 Section 8.
-	payload[0] = 0
-	marshalString(payload[1:], []byte(c.config.Password))
-
-	return c.writePacket(marshal(msgUserAuthRequest, userAuthRequestMsg{
-		User:    c.config.User,
-		Service: serviceSSH,
-		Method:  method,
-		Payload: payload,
-	}))
-}
-
 // kexDH performs Diffie-Hellman key agreement on a ClientConn. The
 // returned values are given the same names as in RFC 4253, section 8.
 func (c *ClientConn) kexDH(group *dhGroup, hashFunc crypto.Hash, magics *handshakeMagics, hostKeyAlgo string) ([]byte, []byte, error) {
@@ -348,8 +298,9 @@ type ClientConfig struct {
 	// The username to authenticate.
 	User string
 
-	// Used for "password" method authentication.
-	Password string
+	// A slice of ClientAuth methods. Only the first instance 
+	// of a particular RFC 4252 method will be used during authentication.
+	Auth []ClientAuth
 }
 
 func (c *ClientConfig) rand() io.Reader {

authenticate 関数と sendUserAuthReq 関数が削除され、認証ロジックが client_auth.go に移動しました。 ClientConfig 構造体から Password フィールドが削除され、代わりに Auth []ClientAuth フィールドが追加されました。

src/pkg/exp/ssh/client_auth.go (新規ファイル)

--- /dev/null
+++ b/src/pkg/exp/ssh/client_auth.go
@@ -0,0 +1,157 @@
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package ssh
+
+import (
+	"errors"
+)
+
+// authenticate authenticates with the remote server. See RFC 4252. 
+func (c *ClientConn) authenticate() error {
+	// initiate user auth session
+	if err := c.writePacket(marshal(msgServiceRequest, serviceUserAuth)); err != nil {
+		return err
+	}
+	packet, err := c.readPacket()
+	if err != nil {
+		return err
+	}
+	var serviceAccept serviceAcceptMsg
+	if err := unmarshal(&serviceAccept, packet, msgServiceAccept); err != nil {
+		return err
+	}
+	// during the authentication phase the client first attempts the "none" method
+	// then any untried methods suggested by the server. 
+	tried, remain := make(map[string]bool), make(map[string]bool)
+	for auth := ClientAuth(new(noneAuth)); auth != nil; {
+		ok, methods, err := auth.auth(c.config.User, c.transport)
+		if err != nil {
+			return err
+		}
+		if ok {
+			// success
+			return nil
+		}
+		tried[auth.method()] = true
+		delete(remain, auth.method())
+		for _, meth := range methods {
+			if tried[meth] {
+				// if we've tried meth already, skip it.
+				continue
+			}
+			remain[meth] = true
+		}
+		auth = nil
+		for _, a := range c.config.Auth {
+			if remain[a.method()] {
+				auth = a
+				break
+			}
+		}
+	}
+	return errors.New("ssh: unable to authenticate, no supported methods remain")
+}
+
+// A ClientAuth represents an instance of an RFC 4252 authentication method.
+type ClientAuth interface {
+	// auth authenticates user over transport t. 
+	// Returns true if authentication is successful.
+	// If authentication is not successful, a []string of alternative 
+	// method names is returned.
+	auth(user string, t *transport) (bool, []string, error)
+
+	// method returns the RFC 4252 method name.
+	method() string
+}
+
+// "none" authentication, RFC 4252 section 5.2.
+type noneAuth int
+
+func (n *noneAuth) auth(user string, t *transport) (bool, []string, error) {
+	if err := t.writePacket(marshal(msgUserAuthRequest, userAuthRequestMsg{
+		User:    user,
+		Service: serviceSSH,
+		Method:  "none",
+	})); err != nil {
+		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]}
+}
+
+func (n *noneAuth) method() string {
+	return "none"
+}
+
+// "password" authentication, RFC 4252 Section 8.
+type passwordAuth struct {
+	ClientPassword
+}
+
+func (p *passwordAuth) auth(user string, t *transport) (bool, []string, error) {
+	type passwordAuthMsg struct {
+		User     string
+		Service  string
+		Method   string
+		Reply    bool
+		Password string
+	}
+
+	pw, err := p.Password(user)
+	if err != nil {
+		return false, nil, err
+	}
+
+	if err := t.writePacket(marshal(msgUserAuthRequest, passwordAuthMsg{
+		User:     user,
+		Service:  serviceSSH,
+		Method:   "password",
+		Reply:    false,
+		Password: pw,
+	})); err != nil {
+		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]}
+}
+
+func (p *passwordAuth) method() string {
+	return "password"
+}
+
+// A ClientPassword implements access to a client's passwords.
+type ClientPassword interface {
+	// Password returns the password to use for user.
+	Password(user string) (password string, err error)
+}
+
+// ClientAuthPassword returns a ClientAuth using password authentication.
+func ClientAuthPassword(impl ClientPassword) ClientAuth {
+	return &passwordAuth{impl}
+}

このファイルは、新しい認証フレームワークの核心部分です。

  • authenticate 関数がここに移動し、認証ネゴシエーションロジックが実装されました。
  • ClientAuth インターフェースが定義されました。
  • noneAuthpasswordAuth という ClientAuth インターフェースの実装が提供されました。
  • ClientPassword インターフェースと ClientAuthPassword ヘルパー関数が追加されました。

src/pkg/exp/ssh/doc.go

--- a/src/pkg/exp/ssh/doc.go
+++ b/src/pkg/exp/ssh/doc.go
@@ -83,7 +83,7 @@ authentication method is supported.
 
 	config := &ClientConfig{
 		User: "username",
-		Password: "123456",
+		Auth: []ClientAuth{ ... },
 	}
 	client, err := Dial("yourserver.com:22", config)
 

ドキュメントの例が更新され、ClientConfigPassword フィールドの代わりに Auth フィールドを使用する新しい認証方法が示されています。

コアとなるコードの解説

ClientAuth インターフェース

type ClientAuth interface {
	auth(user string, t *transport) (bool, []string, error)
	method() string
}

このインターフェースは、SSH認証メソッドの抽象化を提供します。auth メソッドは実際の認証ロジックを実行し、成功したかどうか、およびサーバーが提示する代替認証メソッドのリストを返します。method メソッドは、その認証方法のRFC 4252で定義された名前を返します。

authenticate 関数 (in client_auth.go)

func (c *ClientConn) authenticate() error {
	// ... (service request/accept logic) ...

	tried, remain := make(map[string]bool), make(map[string]bool)
	for auth := ClientAuth(new(noneAuth)); auth != nil; {
		ok, methods, err := auth.auth(c.config.User, c.transport)
		if err != nil {
			return err
		}
		if ok {
			return nil // Authentication successful
		}
		tried[auth.method()] = true
		delete(remain, auth.method())
		for _, meth := range methods {
			if tried[meth] {
				continue
			}
			remain[meth] = true
		}
		auth = nil
		for _, a := range c.config.Auth {
			if remain[a.method()] {
				auth = a
				break
			}
		}
	}
	return errors.New("ssh: unable to authenticate, no supported methods remain")
}

この関数は、SSHクライアント認証の主要なロジックをカプセル化しています。

  1. まず、SSHユーザー認証サービスを要求します。
  2. 次に、noneAuth メソッドから認証試行を開始します。これは、サーバーがサポートする認証方法のリストを取得するためによく使われます。
  3. 認証が成功しなかった場合、サーバーから返された利用可能な認証メソッドのリスト (methods) を確認します。
  4. c.config.Auth に設定された認証メソッドのリストを繰り返し処理し、まだ試行しておらず、かつサーバーがサポートしているメソッドを次に試行します。
  5. このループは、認証が成功するか、試行できる認証メソッドがなくなるまで続きます。

noneAuthpasswordAuth の実装

これらの構造体は ClientAuth インターフェースを実装し、それぞれの認証方法の具体的なロジックを提供します。passwordAuthClientPassword インターフェースを介してパスワードを取得するため、パスワードの供給源を柔軟に設定できます。

ClientConfig の変更

type ClientConfig struct {
	User string
	Auth []ClientAuth // New field
}

Auth フィールドは、クライアントが認証時に試行する ClientAuth インターフェースを実装したオブジェクトのリストを保持します。これにより、ユーザーは複数の認証方法(例: まず公開鍵、次にパスワード)を定義し、SSHクライアントがそれらを順番に試行するように設定できます。

関連リンク

参考にした情報源リンク

  • RFC 4252 - The Secure Shell (SSH) Authentication Protocol
  • Go言語のドキュメントとソースコード
  • SSHプロトコルに関する一般的な知識
  • Go言語のインターフェースに関する知識
  • golang.org/x/crypto/ssh パッケージの現在の実装 (参考として)
  • Go.dev: golang.org/x/crypto/ssh (https://pkg.go.dev/golang.org/x/crypto/ssh)