[インデックス 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クライアント認証のメカニズムを、固定的なパスワード認証から、より柔軟なプラグイン可能な認証方法のフレームワークへと移行したことです。
-
ClientAuth
インターフェースの導入:ClientAuth
インターフェースは、SSH認証メソッドのインスタンスを表します。- このインターフェースは2つのメソッドを定義します:
auth(user string, t *transport) (bool, []string, error)
: ユーザー認証を試行します。認証が成功した場合はtrue
を返し、失敗した場合はサーバーが提示する代替認証メソッドのリストを返します。method() string
: RFC 4252で定義されている認証メソッド名(例: "none", "password")を返します。
- これにより、異なる認証方法(例:
noneAuth
,passwordAuth
)がこのインターフェースを実装することで、統一された方法で認証処理を呼び出すことが可能になります。
-
ClientConfig
構造体の変更:- 以前は
Password string
フィールドでパスワードを直接保持していましたが、これが削除されました。 - 代わりに
Auth []ClientAuth
フィールドが追加されました。これは、クライアントが試行する認証メソッドのリストを保持します。これにより、複数の認証方法を順番に試すことが可能になります。
- 以前は
-
authenticate
メソッドのリファクタリング:ClientConn
のauthenticate
メソッドが大幅にリファクタリングされました。- 新しい
authenticate
メソッドは、まず「none」認証を試行します。 - 「none」認証が失敗し、サーバーが利用可能な認証メソッドのリストを返した場合、クライアントは
ClientConfig.Auth
に設定された認証メソッドの中から、まだ試行しておらず、かつサーバーがサポートしているメソッドを順番に試行します。 - このプロセスは、認証が成功するか、利用可能な認証メソッドがなくなるまで繰り返されます。
-
noneAuth
とpasswordAuth
の実装:noneAuth
はClientAuth
インターフェースを実装し、RFC 4252の「none」認証を処理します。passwordAuth
はClientAuth
インターフェースを実装し、RFC 4252の「password」認証を処理します。passwordAuth
はClientPassword
インターフェース(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
インターフェースが定義されました。noneAuth
とpasswordAuth
という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)
ドキュメントの例が更新され、ClientConfig
の Password
フィールドの代わりに 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クライアント認証の主要なロジックをカプセル化しています。
- まず、SSHユーザー認証サービスを要求します。
- 次に、
noneAuth
メソッドから認証試行を開始します。これは、サーバーがサポートする認証方法のリストを取得するためによく使われます。 - 認証が成功しなかった場合、サーバーから返された利用可能な認証メソッドのリスト (
methods
) を確認します。 c.config.Auth
に設定された認証メソッドのリストを繰り返し処理し、まだ試行しておらず、かつサーバーがサポートしているメソッドを次に試行します。- このループは、認証が成功するか、試行できる認証メソッドがなくなるまで続きます。
noneAuth
と passwordAuth
の実装
これらの構造体は ClientAuth
インターフェースを実装し、それぞれの認証方法の具体的なロジックを提供します。passwordAuth
は ClientPassword
インターフェースを介してパスワードを取得するため、パスワードの供給源を柔軟に設定できます。
ClientConfig
の変更
type ClientConfig struct {
User string
Auth []ClientAuth // New field
}
Auth
フィールドは、クライアントが認証時に試行する ClientAuth
インターフェースを実装したオブジェクトのリストを保持します。これにより、ユーザーは複数の認証方法(例: まず公開鍵、次にパスワード)を定義し、SSHクライアントがそれらを順番に試行するように設定できます。
関連リンク
- RFC 4252 - The Secure Shell (SSH) Authentication Protocol: https://datatracker.ietf.org/doc/html/rfc4252
- Go言語の
crypto/ssh
パッケージ (現在の標準ライブラリ): https://pkg.go.dev/golang.org/x/crypto/ssh (このコミットのexp/ssh
は、後にgolang.org/x/crypto/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クライアント認証のメカニズムを、固定的なパスワード認証から、より柔軟なプラグイン可能な認証方法のフレームワークへと移行したことです。
-
ClientAuth
インターフェースの導入:ClientAuth
インターフェースは、SSH認証メソッドのインスタンスを表します。- このインターフェースは2つのメソッドを定義します:
auth(user string, t *transport) (bool, []string, error)
: ユーザー認証を試行します。認証が成功した場合はtrue
を返し、失敗した場合はサーバーが提示する代替認証メソッドのリストを返します。method() string
: RFC 4252で定義されている認証メソッド名(例: "none", "password")を返します。
- これにより、異なる認証方法(例:
noneAuth
,passwordAuth
)がこのインターフェースを実装することで、統一された方法で認証処理を呼び出すことが可能になります。
-
ClientConfig
構造体の変更:- 以前は
Password string
フィールドでパスワードを直接保持していましたが、これが削除されました。 - 代わりに
Auth []ClientAuth
フィールドが追加されました。これは、クライアントが試行する認証メソッドのリストを保持します。これにより、複数の認証方法を順番に試すことが可能になります。
- 以前は
-
authenticate
メソッドのリファクタリング:ClientConn
のauthenticate
メソッドが大幅にリファクタリングされました。- 新しい
authenticate
メソッドは、まず「none」認証を試行します。 - 「none」認証が失敗し、サーバーが利用可能な認証メソッドのリストを返した場合、クライアントは
ClientConfig.Auth
に設定された認証メソッドの中から、まだ試行しておらず、かつサーバーがサポートしているメソッドを順番に試行します。 - このプロセスは、認証が成功するか、利用可能な認証メソッドがなくなるまで繰り返されます。
-
noneAuth
とpasswordAuth
の実装:noneAuth
はClientAuth
インターフェースを実装し、RFC 4252の「none」認証を処理します。passwordAuth
はClientAuth
インターフェースを実装し、RFC 4252の「password」認証を処理します。passwordAuth
はClientPassword
インターフェース(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
インターフェースが定義されました。noneAuth
とpasswordAuth
という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)
ドキュメントの例が更新され、ClientConfig
の Password
フィールドの代わりに 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クライアント認証の主要なロジックをカプセル化しています。
- まず、SSHユーザー認証サービスを要求します。
- 次に、
noneAuth
メソッドから認証試行を開始します。これは、サーバーがサポートする認証方法のリストを取得するためによく使われます。 - 認証が成功しなかった場合、サーバーから返された利用可能な認証メソッドのリスト (
methods
) を確認します。 c.config.Auth
に設定された認証メソッドのリストを繰り返し処理し、まだ試行しておらず、かつサーバーがサポートしているメソッドを次に試行します。- このループは、認証が成功するか、試行できる認証メソッドがなくなるまで続きます。
noneAuth
と passwordAuth
の実装
これらの構造体は ClientAuth
インターフェースを実装し、それぞれの認証方法の具体的なロジックを提供します。passwordAuth
は ClientPassword
インターフェースを介してパスワードを取得するため、パスワードの供給源を柔軟に設定できます。
ClientConfig
の変更
type ClientConfig struct {
User string
Auth []ClientAuth // New field
}
Auth
フィールドは、クライアントが認証時に試行する ClientAuth
インターフェースを実装したオブジェクトのリストを保持します。これにより、ユーザーは複数の認証方法(例: まず公開鍵、次にパスワード)を定義し、SSHクライアントがそれらを順番に試行するように設定できます。
関連リンク
- RFC 4252 - The Secure Shell (SSH) Authentication Protocol: https://datatracker.ietf.org/doc/html/rfc4252
- Go言語の
crypto/ssh
パッケージ (現在の標準ライブラリ): https://pkg.go.dev/golang.org/x/crypto/ssh (このコミットのexp/ssh
は、後にgolang.org/x/crypto/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)